首页 > 编程语言 >Java 多线程编程基础

Java 多线程编程基础

时间:2024-05-25 16:44:57浏览次数:28  
标签:同步 Java 变量 synchronized 编程 阻塞 线程 内存 多线程

我们的应用程序都是运行在多线程的环境下的,在多线程环境下的许多问题我们都了解吗?

  • 线程间如何进行数据交换?
  • 线程间如何进行通信与协作?
  • 共享一个资源时如何保证线程安全?

线程数据交换

线程之间无法直接访问对方工作内存中的变量,必须通过主内存进行变量的传递。例如,线程A、B共享一个变量C,当A在工作内存中更新了C的值,并同步到主内存后,线程B才能从主内存中获取到变量C的最新值。

内存模型

要理解线程间的数据访问,首先得知道Java中的内存模型,其主要目的是定义程序中各种变量的访问规则。类似于计算机中的高速缓存和主内存,Java中的内存模型将内存划分为主内存和工作内存。

其中,主内存是所有线程共享的,每个线程拥有一个专属的工作内存。

  • 主内存:用于存储所有的变量
  • 工作内存:用于存储线程所使用变量的主内存副本

线程对变量的所有操作(读取、赋值等)都在工作内存中进行。例如,当线程赋值给字段时,会先在工作内存中进行更改,然后择机将更改同步到主内存中,这个时机由JVM决定。

线程安全

线程安全就是在多个线程间共享一个资源时,保证每个线程都能读取到正确状态的数据。简单来说就是同一时间只能有一个线程具有该共享资源更改或者访问权限。

那如何保证线程安全呢?实现策略无非就两种

  • 一是基于悲观锁的阻塞同步
  • 二是基于乐观锁的非阻塞同步

阻塞同步

阻塞同步,即同一时间,只能有一个线程有访问权限。java中阻塞同步的方式有两种,一是使用synchronized关键词、二是使用ReentrantLock。在访问被synchronized或ReentrantLock锁定的区域时,需要先获取对应的锁资源,并且同时只允许一个线程持有锁。在一个线程持有锁后,其他线程需要阻塞等待,直到可获取时被手动或者被动唤醒。

在被锁定的区域中(临界区),如果存在对共享变量的更改,在锁释放后,这个更改后立即刷新到主内存中,其他线程可以立即看到,这个也叫做可见性。

synchronized

synchronized是一个关键字,JVM级别的锁。可以修饰在方法上,表示方法为同步方法,也可以修饰在代码块上。但需要明白的是synchronized是锁在一个对象上的,修饰方法时,代表锁定的是当前实例。

那如果修饰在静态方法或者静态代码块上,锁定的是什么对象?因为与实例无关,肯定不是锁定在实例对象上,而是锁定在一个Class对象上,Class对象在内存中的表示就是一个字节码对象。通过反射机制获取类的字段、方法,就是操作的字节码对象。

如果一个类中多个方法都被synchronized修饰,那么同一时间,只能有一个方法被执行。

public synchronized String getConfig() {

    return "hello";
}

如果在一个类中存在多个竞争资源且无关联时,需要为不同的资源设置不同的锁对象。如果都使用当前对象,可能对系统性能造成负面影响。

private final Object LOCK_CONFIG= new Object();

public String getConfig() {

    Object o = new Object();
    synchronized (LOCK_CONFIG) {
        return "hello";
    }
}
ReentrantLock

与synchronized不同的是,ReentrantLock本身就是一个锁对象,创建后直接调用lock、unlock即可加解锁。ReentrantLock基于同步器AQS(AbstractQueuedSynchronizer)实现。

AQS内部维护了一个双向链表(CLH同步队列),用于保存等待获取锁的线程。这个队列采用FIFO(先进先出)的顺序,可以保证线程获取锁的公平性。

AQS中通过一个整型的volatile变量来表示同步状态,支持独占式同步(独占锁)和共享式同步(共享锁)。

ReentrantLock、Semaphore、CountDownLatch均是基于AQS实现。

简单使用

private final ReentrantLock reentrantLock = new ReentrantLock();

public String getConfig() {
    
    reentrantLock.lock();
    try {
        return "hello";
    } finally {
        reentrantLock.unlock();
    }
}

高级应用

相较于synchronized,ReentrantLock功能更加强大,可以实现公平锁(默认为非公平锁实现,公平锁影响性能),可以通过newCondition()方法创建一个Condition对象,用于在某些条件下挂起和唤醒线程。

Condition接口提供了以下几个重要方法:

  • await():当前线程等待,同时释放锁,直到其他线程调用signal()或signalAll()方法唤醒它。
  • signal():唤醒一个等待在该条件上的线程。
  • signalAll():唤醒所有等待在该条件上的线程。

在生产者-消费者模式下的阻塞队列中,使用ReentrantLock和Condition,可以更加灵活的控制线程间的协作。详见java.util.concurrent.LinkedBlockingQueue实现。

非阻塞同步

非阻塞同步是一种乐观策略,假设不存在并发冲突,先进行操作。这时有两种情况,一是没有并发冲突,那很完美,直接执行;如果存在并发冲突,再进行补偿操作。整个过程不需要加锁控制,所以被称为无锁编程。

非阻塞同步要求操作和冲突检测这两个步骤具备原子性和共享变量的可见性。

  • 原子性:指一个操作是不可中断的,要么全部成功,要么全部失败,在多线程执行时,一个线程原子操作开始,不会被其他线程所干扰。Java中基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子性协定),synchronized关键字也实现了原子性。
  • 可见性:可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

同步阻塞的加锁也保证了原子性(只有一个线程操作,不会被其他线程干扰)和可见性(释放锁时立即刷新到主存)。

CAS

java中是CAS(Compare-and-Swap,比较并交换)操作保证了原子性,执行时需要三个参数:

  • 一是变量的内存地址,以V代指
  • 二是旧值,以A代指
  • 三是新值,以B代指

在更新时,只有A值等于V值时,才将V值更新为B值,否则不进行更新操作。

在sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供了CAS操作。

volatile

java中,使用volatile修饰的变量,可以保证其可见性。

线程在变量读取前会先从主内存刷新变量值,无论是普通变量还是volatile变量都是如此。但区别是,volatile变量保证了值在工作内存中更改后立即同步到主内存,而普通变量的更改则由JVM选择择机同步到主内存。

使用实例

java提供了一个java.util.concurrent.atomic包,这个包中的类都是线程安全的,实现方式为非阻塞的无锁实现。其中原子性通过CAS操作保证,而可见性通过volatile关键字实现。

  • 在多线程环境下,使用AtomicInteger进行计数
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger counter = new AtomicInteger(0);
    private static final int NUM_THREADS = 10;
    private static final int NUM_INCREMENTS = 1000;

    public static void main(String[] args) {
        Thread[] threads = new Thread[NUM_THREADS];

        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < NUM_INCREMENTS; j++) {
                    counter.incrementAndGet();
                }
            });
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (int i = 0; i < NUM_THREADS; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final counter value: " + counter.get());
    }
}

线程协作/通信

在阻塞同步时,多个线程竞争一个锁资源,在第一个线程获取到锁后,其余线程全被阻塞。在第一个线程后执行完成后,可以唤醒阻塞在该锁上的线程。

每一个Object都可以作为一个锁,锁对象中提供了一些方法可用于线程间通信

wait()

使当前线程进入等待状态,并释放所持有的锁。

在没有设置timeout时,需要等待其他线程调用了相同对象上的notify()或notifyAll()方法来唤醒它;在设置timeout后,时间到了之后由VM唤醒。需要注意的是,被唤醒并重新获取锁后,其实是从wait方法处返回,继续往下执行同步代码块中的代码。

notify()

用于唤醒等待该对象锁的线程集合中的一个线程(将其从阻塞状态转换为就绪状态),具体是哪一个线程被唤醒是不确定的。

调用notify()后,当前线程 不会释放所持有的锁 ,而是需要执行完同步代码块后才释放。被唤醒的线程会尝试重新获取对象的锁,一旦获取到锁,它就会从wait()方法返回,继续执行。

notifyAll()

用于唤醒等待该对象锁的线程集合中的所有线程。

标签:同步,Java,变量,synchronized,编程,阻塞,线程,内存,多线程
From: https://www.cnblogs.com/cd-along/p/18211133

相关文章

  • Java ThreadPoolExecutor
    ThreadPoolExecutor?ThreadPoolExecutor是什么,先拆开来看,ThreadPoolAndExecutor?那ThreadPool是什么?Executor又是什么?Executor:任务执行者,只定义了一个execute方法,接收一个Runable参数。publicinterfaceExecutor{voidexecute(Runnablecommand);}ThreadPool:可以缓存......
  • Java实现图书系统
    首先实现一个图书管理系统,我们要知道有哪些元素?1.用户分成为管理员和普通用户2.书:书架  书3.操作的是:书架目录第一步:建包第二步:搭建框架首先:完成book中的方法其次:完成BookList然后:完成管理员界面和普通用户界面最后:Main第三步:细分方法1.退出系统2......
  • 从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?
    前言大家好,我是老马。很高兴遇到你。作为一个java开发者,工作中一直在使用nginx。却发现一直停留在使用层面,无法深入理解。有一天我在想,为什么不能有一个java版本的nginx呢?一者是理解nginx的设计灵魂,再者java开发者用java语言的服务器不是更加自然吗。于是动手开......
  • arthas:Java调试利器,线上Debug不是梦
    目录前言一、Arthas是什么?二、Arthas能解决啥问题?三、Arthas两种安装、启动方式1、jar包启动2、在线安装3、远程连接:四、Arthas命令使用1、Dashboard命令2、Thread(线程监控)3、JVM(jvm实时运行状态,内存使用情况等)4、trace(当前方法内部调用路径,路径上每个节......
  • 「终极收藏」前端开发必备:超全JavaScript公共方法大全
    目录引言1安装js-tool-big-box工具包1.1安装1.2截至目前的方法集合 2时间日期类 2.1更灵活的年月日时分秒2.2 日期时间转换2.3个性的时间组合 2.4 某个时间距离现在2.5 平年还是闰年2.6指定月份的天数 2.7属相2.8获取指定年份的法定节假日 3......
  • python多线程
    1、当程序中有耗时操作时,我们应该使用多线程来进行操作。多线程就像是多辆火车,可以在不同的轨道上同时运行。而进程就像是火车站,正在运行的一个程序的实例。python中多线程可以提供threading模块来实现。简单的多线程案例:importthreadingimporttime#定义线程执行的......
  • Java根据URL下载文件到本地的2种方式(大型文件与小型文件)
    1.小型文件推荐使用2.大型文件推荐使用总结 各位小伙伴是否有使用java,根据url下载文件到本地的需求,以下介绍两种方式1.小型文件推荐使用代码解析首先创建了一个URL对象website,用来表示远程文件的地址。然后创建了一个ReadableByteChannel对象rbc和一个FileOutputStr......
  • Java 登录错误次数限制,用户禁登1小时
    手机号验证码登录,验证码输入错误次数超5次封禁@OverridepublicbooleancheckCaptcha(StringphoneNum,Stringcaptcha){StringcodeNum=(String)redisTemplate.opsForValue().get(UserCacheNames.USER_CAPTCHA+phoneNum);if(codeNum==......
  • 多线程抢沙包游戏
    幼儿园玩抢沙包游戏,共计100个沙包,有10个小朋友(4男6女),男生每次拿3个沙包,女生每次拿2个沙包,如果剩余的沙包不够每次拿的数量,则游戏停止,请用java多线程模拟上述游戏过程。importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.CountDownLatch;import......
  • Shell编程之条件语句
    一、条件测试操作        要使Shell脚本程序具备一定的“智能”,面临的第一个问题就是如何区分不同的情况以确定执行何种操作。例如,当磁盘使用率超过95%时,发送告警信息;当备份目录不存在时,能够自动创建;当源码编译程序时,若配置失败则不再继续安装等。      ......