多并发——总结

1.进程,线程

1.1 区别

进程:程序的一次执行过程,是系统运行程序的基本单位,是进行资源分配的最小单位。

线程:是进程划分的更小的运行单位,一个进程可以产生多个线程。多个线程共享进程的堆和方法区资源,但是每个线程都有自己的程序计数器,虚拟机栈和本地方法栈

区别:进程之间是独立的,线程不一定。线程执行开销小,不利于资源的管理和保护

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。一个 Java 程序的运行是 main 线程和多个其他线程同时运行

1.2 线程的状态及转换

线程的六种状态:初始,就绪,运行,阻塞,等待,终止

线程创建后就处于初始状态,调用了start方法后,线程处于就绪状态,当获得CPU时间片后,就处于运行状态。

线程执行wait方法后,线程进入等待状态,需要依靠其他线程的通知才能回到运行状态。

阻塞状态的线程,当获得到临界区资源或者是被其他线程唤醒,就可以进入就绪状态,当获得时间片资源后就会进入运行状态

1.3 上下文切换

上下文切换(多线程中线程的个数一般会多于CPU核心的个数,一个CPU核心在一个时刻只能被一个线程使用,所以CPU采用的措施是给每个线程分配时间片轮转的形式)  当前任务执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次回到这个任务的时候,可以再次加载这个状态,任务从保存到加载这个过程就是一次时间片切换

2.start方法和run方法

创建一个线程处于新建状态,然后调用start方法,会启动一个线程进入就绪状态,当获得了时间片后就可以开始运行了。Start方法会执行线程的准备工作,然后自动执行run方法。直接执行run方法,这个方法就先相当于线程下的一个普通方法。

3.sleep方法和wait方法的区别

(1)sleep和wait都可以暂停线程的运行

(2)sleep没有释放锁,wait释放了锁、

(3)wait用于线程交互通信,sleep只是用于暂停执行

(4)wait方法被调用后,需要别的线程调用同一个对象的notify方法或者notifyALL方法唤醒。sleep方法执行完成后,会自动苏醒,

(5)wait方法是Object的一个方法,notify方法是Thread类的一个静态方法

4.sychronized以及底层实现——Java原生的锁

4.1 使用

 解决多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只有一个线程执行。 三种使用方式:

       修饰实例方法:给当前实例加锁

       修饰静态方法:  给当前类加锁

       修饰代码块:  指定加锁对象

        可以看出这个就是给类和对象加锁。

4.2 底层实现(给方法和代码块加锁和释放)

4.2.1 加锁的底层实现

synchronized有两种方式上锁,一种是对方法,一种是对代码块。底层实现原理都是在进入同步代码之前先获取锁,获得锁后锁的计数器加一,同步完代码执行锁的计数器减1。如果获取失败就是阻塞等待。

同步代码块的识别方式不同(通过字节码文件可以看出):

同步方法:flags标志中多一个ACC_SYNCHRONIZED标志,这个标志告诉JVM还是一个同步方法,在进入该方法先获得相应的锁,锁的计数器加一,方法结束后减1,失败就阻塞,直到这个锁被释放。

同步代码块:使用monitorenter和monitorexit指令。monitorenter指令指向同步代码块开始的位置,monitorexit执行同步代码块结束的位置。当执行monitorenter指令时,试图获取monitor的持有权(锁,这个锁位于Java对象的对象头中)。当计数器为0时可以获得,计数器加一。在执行完monitorexit后计数器设置为0,表示锁被释放。如果获取对象失败,当前线程就要被阻塞等待,直到另一个线程被释放。

4.2.2 锁的释放

在程序中,无法显式释放对同步监视器的锁,在四种情况下释放锁:

(1)当前线程的同步方法,同步代码块执行结束的时候释放

(2)当前线程在同步方法,同步代码块中遇到了break,return终结该代码块和方法的时候释放。

(3)出现了未处理的error和exception导致了异常结束时候释放。

(4)程序执行了同步对象的wait方法,当前线程暂停,释放锁

在下面两种情况下不会释放锁:

(1)代码块中使用了Thread.sleep(),Thread.yileld()这些方法暂停线程的执行,但是不会释放锁

(2)线程执行同步代码块时,其他线程调用suspend方法将线程挂起,该线程不会释放锁,所以我们应该避免使用suspend和resume来控制线程。

4.3 sychronized的自旋锁、偏向锁、轻量级锁、重量级锁,分别介绍和联系(膨胀)

偏向锁:偏向锁会偏向第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程将永远不会触发同步。也就是在无资源竞争的情况下,消除了同步语句。

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。当下次该线程进入的时候,会检查锁的mark word是否存放的是自己的线程ID。如果是的话,则进出同步块不需要CAS来加锁和解锁。如果不是,说明有其他线程来竞争偏向锁,这个时候采用CAS替换mark word中的ID为新线程ID。成功,之前的线程不存在了,锁不会升级。失败,之前的线程存在,暂停之前的线程,设置偏向锁标识为0,锁标识为00,升级为轻量级锁,按照轻量级锁竞争。

轻量级锁:多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。采用轻量级锁来避免线程的阻塞或者唤醒。JVM会为每个线程的栈帧创建存储锁记录的空间(Displaced Mark Word),如果一个线程获得锁发现是轻量级锁,会把锁的mark word复制到自己的Displaced Mark Word中。然后线程尝试用CAS将锁的Mark Word替换成指向所记录的指针。如果成功,表示当前线程获得锁。失败,表示Mark Word已经被替换成其他线程的锁记录,说明存在竞争,当前线程就需要自旋获得锁。依然没有获得,就说明失败,这个线程阻塞,锁升级为重量级锁。

重量级锁:是依赖对象内部的monitor锁来实现的,而monitor依靠操作系统的互斥了mutex实现的,操作系统中线程转换需要较长的时间,所以重量锁的效率低。重量级锁就是我们传统上的锁。队列操作:竞争队列....队列操作

      自旋锁:所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。

       自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。其大概原理是这样的:假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。

4.4 synchronized如何保证三大特性

4.4.1 可见性

JMM中的happens-before规则中有一条监视器规则:对同一个监视器的解锁,happens-before于对该监视器的加锁

        (1)线程解锁前,必须把工作内存中共享变量的最新值刷新到主内存中
        (2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值(注意:加锁与解锁需要是同一把锁)

4.4.2 原子性(多个线程使用一个对象锁,这样临界区资源可以看做一个院子操作)

synchronized实现原子性需要多个线程之间使用相同的对象锁。这样临界区里所有的就代码可以看做一个原子操作。比如: A、B两个线程都使用 Object.class 对象来做对象锁。那么B线程无法读到 A线程的中间状态。如果使用的是不同的对象锁或者有一个线程不使用synchronized,那么就不存在原子性!

4.4.3 有序性(每次都只有一个线程执行,程序的执行结果不能改变)

synchronized不能防止指令重排序,但是能保证有序性,这和volatile实现有序性的方式不同

synchronized是通过互斥锁来保证有序性的,同步块里是单线程的。按照as-if-serial语义:即在单线程下不管怎么重排序,程序的执行结果不能被改变。

5.乐观锁和悲观锁

5.1 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

5.2 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

5.2.1 版本机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

5.2.2 cas算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

5.2.3 ABA问题

  如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

解决办法:

 (1)使用版本号

 (2)JDK 1.5 以后的 AtomicStampedRe

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Python爬虫多线程并发是指使用多个线程同时执行爬取任务,以提高爬取效率。通过并发执行,可以在同一时间内处理多个请求和响应,从而加快网页的下载和解析过程。 在Python中,可以使用多种方式实现爬虫的多线程并发。其中一种常见的方式是使用`threading`模块创建线程并管理线程的执行。通过创建多个线程,每个线程负责执行一个爬取任务,可以同时进行多个任务的爬取,提高整体的效率。 另一种方式是使用线程池。线程池可以预先创建一定数量的线程,并将任务分发给这些线程进行执行。通过线程池,可以有效地管理线程的创建和销毁,避免频繁地创建和销毁线程带来的开销。 多线程并发爬取的优点包括提高爬取效率、缩短爬取时间,同时还可以更好地利用计算机的多核处理能力。然而,需要注意的是,在进行多线程并发爬取时,需要考虑线程安全性和资源竞争的问题,避免出现数据错乱或者死锁等问题。 总结来说,Python爬虫多线程并发是一种提高爬取效率的方法,通过同时执行多个爬取任务,可以加快网页的下载和解析过程,从而更快地获取所需的数据。可以使用`threading`模块或线程池来实现多线程并发,但需要注意线程安全性和资源竞争的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Python并发编程相关及在爬虫实战中的使用](https://blog.csdn.net/weixin_44327634/article/details/123948849)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [python并发爬虫——多线程、线程池实现](https://blog.csdn.net/sixteen_16/article/details/116176587)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值