WEB入门浅谈20

Posted LIT-涛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WEB入门浅谈20相关的知识,希望对你有一定的参考价值。

WEB入门浅谈20

多线程

锁策略

乐/悲观锁

乐观锁:出现锁竞争的概率较低(线程少,不太涉及锁竞争)
悲观锁:出现锁竞争的概率较高(线程多,很可能涉及锁竞争)
在操作系统中提供的锁接口,Mutex(互斥量,操作系统的锁),就是一个典型的悲观锁,认为竞争很大,一旦竞争,那么就有线程阻塞,进入等待,而什么时候被唤醒,就要看调度器的实现了
在应用程序里面,还可以通过一些其它的方式实现锁(如:CAS机制),相当于仅仅一个 用户态的锁,不太涉及到内核和操作系统之间的切换,也就更高效一点
而Java中的synchronized即是悲观锁,也是乐观锁,可以针对当前锁冲突的情况,自动的切换模式,而这种类型的锁,也叫做 自适应 的锁

读写锁

普通锁提供两个操作:加锁、释放锁
读写锁提供三个操作:读加锁、写加锁、释放锁
读加锁与读加锁,不进行竞争。
读加锁和写加锁,发生竞争。
写加锁和写加锁,发生竞争。
也就是读的时候可以进行读操作,写的时候不可以进行其它操作
作用就是能进一步的降低锁冲突的概率。
synchronized 不是读写锁

重/轻量级锁

和 乐观锁、悲观锁 比较相似
一般来说:乐观,工作量少。悲观,工作量大。这主要是站在锁冲突概率的角度来看待的
一般来说,乐观锁就是轻量级锁,悲观锁就是重量级锁,但是并不绝对。这主要是站在工作量与消耗资源的消毒来看待的
轻量级锁:工作量小,消耗资源少,锁更快。
重量级锁:工作量大,消耗资源多,锁更慢。
mutex (操作系统的锁)就是一个重量级锁,也是悲观锁,这个锁在加锁的时候遇到冲突,就会产生内核态与用户态的切换,以及线程的阻塞和调度。
而基于 CAS机制 的轻量级锁,在加锁的时候遇到冲突,不会涉及到内核态与用户态的切换,直接尝试重新获取锁,直到获取成功,这个过程没有放弃CPU,不涉及线程调度(这种一直循环获取锁的锁叫自旋锁)
如果当前锁的竞争压力很大,采用自旋锁的策略也不是很好用,如果竞争压力小,那么采用自旋锁就比较舒服
synchronized 是自适应锁,开始的时候是轻量级锁,如果竞争激烈,就会编程重量级锁

(非)公平锁

符合先来后到的规则的锁就叫公平锁,有线程会进行插队就叫非公平锁,synchronized 就是非公平锁
系统中对于线程的调度就是一个随机的过程(先来的和后到的线程获取锁的机会是均等的),而这种就属于非公平锁,默认不做任何处理,就是非公平锁
自旋锁也是非公平锁

(不)可重入锁

可重入锁:一个线程,连续对同一把锁加锁两次(针对a加锁,锁内还有一个锁,也是针对a的锁)如果第二个锁能获取到锁,就是可重入锁。
不可重入锁:如果第二个锁不能获取到锁,就是不可重入锁。
synchronized 就是可重入锁(通过计数器来确定是否释放所有的锁)
而如果是不可重入锁,就会进入一个死锁,线程无法继续工作,一直处于阻塞状态,就叫做进入了死锁状态

死锁

产生死锁就表示线程进入了无限等待(阻塞)的状态,无法继续工作(很严重给的BUG)
死锁的几个典型场景
一个线程1把锁:一把锁连续加两次,就进入了无限阻塞状态(可重入锁不涉及这种情况)
两个线程2把锁:线程1获取到锁1,同时线程2获取到锁2,线程1再获取锁2时就需要等线程2释放锁2,但是线程2此时也要获取锁1,那么两个线程都会进入到无限等待的情况,谁也不让谁,这也是死锁的常见情况之一。而这种代码是很容易(1%的可能)出现死锁的
N个线程M把锁:本质上与两个线程2把锁是一样的,都是进入了锁后再获取锁时进入循环等待的状态,从而无限阻塞

死锁产生的原因:环路等待(核心原因)、不可抢占、互斥使用、请求和保持
死锁的危害:导致严重的BUG(对应的线程无法继续工作)
死锁的解决方案;规避死锁
1、不要在加锁代码中尝试获取其它锁(同一时刻只获取一把锁)
2、约定一种固定的顺序来加锁(如:获取锁1的情况下才能获取锁2,同理)

CAS机制

CAS机制 全称 compare and swap 比较并交换 是一个原子操作(有原子性)
在Java代码中,比较、赋值、等操作都不是原子操作,这样的代码翻译成机器指令后不是单一的机器指令,而是多条指令,多条指令就可能会受到线程调度的影响。
CPU会支持类似SWAP/EXCHANGE这样的指令,能够做到一条指令完成比较和交换的操作
CAS的用法:

	boolean CAS(address,expectedValue,newValue)
		if(*address == expectedValue)
			// 实际上是把这两个变量的内存上的值进行了交换
			*address = newValue;
			return true;
		
		return false;
	

CAS一条指令就可以完成这样的操作

基于 CAS 可以实现一个自旋锁/轻量级锁

class Lock
	Thread owner = null;
	void lock()
		while(!CAS(&owner,null,Thread.currentThread()))
			// 先看owner中记录的值是不是为null,如果为null,为null就表示当前锁没有被其它线程获取,如果没有其它线程获取当前锁,就把当前线程设置到owner里,如果部位null,那么就继续循环。
			// 也就是说这是一个自旋锁,这里只写了加锁方法并没有写解锁方法。
		
	

CAS还可以用来实现原子类(如:AtomicInteger)
原子类:int long 类型的数据在进行操作的时候,很可能不是原子的,原子类能够保证,针对这些数据进行操作时,也都是原子的

ABA问题

在CAS机制中进行比较时,只能看到内存中的当前值,不能直到这个值是否被修改过(修改后又修改回来)
如:
送快递,有一个用户A的快递要去送,但是你中午去吃饭,你朋友去送这个快递,但是被用户A被退回,然后你下午去了之后又看到这个快递,于是你再去送。(快递员表示线程,送快递、退快递表示修改数据)
预期送一次,实际送了两次,不符合预期。
解决方案:
在进行修改数据时,添加一个 上次修改时间 ,CAS在进行数据比较的时候,不仅仅比较数据,也比较 修改时间 ,如果都一致,就表示数据没有被修改过。(不一定是时间,也可以是别的数据,如:版本号)

补充

synchronized:自适应锁(乐观锁或悲观锁,轻量锁或重量锁)、轻量级锁是基于自旋的方式实现的、非公平锁、可重入锁、不是读写锁

mutex:悲观锁、重量级锁、不是自旋锁、非公平锁、不可重入锁、不是读写锁


GPU:显卡,用来计算。在之前都是CPU中包含显卡,也就是CPU和GPU不分家,以前就称为 集成显卡 核心显卡。
虽然都是用来计算的,但是GPU的硬件特性和CPU差异极大,也就导致GPU编程和CPU编程差别也很大。对于Java来说,代码主要是围绕CPU来写的。
而一些游戏开发/人工智能就要用GPU编程来进行开发,编程的思路就是截然不同的了


CAS也可以理解为:进入CAS机制后,其它线程不工作,本线程把要使用的数据先与内存中做一个比较,然后再对数据进行操作,存入内存,退出机制。


WEB入门浅谈17

WEB入门浅谈17

多线程

进程是为了实现并发编程的效果,但为了追求更高的效率就引进了线程
创建一个进程和销毁一个进程,开销比较大(进程管理中存在一些系统分配的资源,申请和释放这些资源不是一个容易的事),因此就希望能够更高效,更轻量的完成并发编程。于是就通过线程来完成
线程也被称为 轻量级进程
每个线程就对应到一个 独立的执行流 ,在这个执行流里就可以完成一系列的指令。有多个线程,就对应的有多个执行流,就可以并发的完成多个系列的指令了

一个进程包含了多个线程。一个进程从操作系统中申请了很多资源,进程统一对这些资源进行管理,这个进程内的多个线程,就共享了这些资源。

进程具有 独立性 ,进程与进程之间没有影响(一个进程的创建和销毁不会影响到其它进程)。而线程则不然,如果一个线程出问题了,就可能会影响整个进程的工作。

单进程,单线程:一个人去做一件事。(效率太低)
多进程,单线程:两个人分开去做一件事。(效率提升了,但是资源需要的就多了)
单进程,多线程:两个人一起来做一件事。(效率提升了多余的资源节省了)

创建线程就比创建进程的开销更低

虽然线程越多,效率可能就会越高。但并不是线程越多越好,一旦线程数量太多就会拥挤不堪,多个线程为了争取CPU资源就会造成更多的开销(线程多了,调度的开销也就大了)

要使效率达到最高,那就需要视情况而定创建多少个线程了。例如:
在一台8核CPU的主机上创建进程,那么创建多少个线程就取决于每个线程的执行的任务里,有多少工作是占用CPU,有多少是等待I/O(阻塞)如果线程的工作是纯CPU(无阻塞),那么线程创建8时,效率达到最高。如果线程只有10%占用CPU,那么线程就可以创建到80达到最高效率。(理论)
但是这样做也会出现几个问题,效率虽然高了,开销也小了,但是在工作时,多个线程可能会出现 争抢问题(抢着干一件事,可以理解为分配不均)。也有在工作时,某个线程一直在就绪状态,但是一直轮不上,此时也会出现问题。这个时候就需要考虑线程安全问题。

Java中为了方便操作线程,有Thread这样的类来表示线程
不同的系统上,对于线程这里提供的API是不一样的。Java中都通过Thread类来包装好了
Java中的一个Thread对象就和操作系统内部的一个线程是一一对应的

调用start才是创建新线程,调用run只是一个简单的函数调用

线程的属性

getId() 获取当前线程id
getName() 获取当前线程名称
getState() 获取当前线程状态(此处状态指的是Java的线程状态,与操作系统中的进程线程状态类似但不一样)
getPriority() 获取当前线程的优先级
isDaemon() 查看当前线程是否为守护线程/后台线程
守护线程:主线程(main)结束后,守护线程也不会继续执行了,进程退出,
前台线程:主线程(main)结束后,前台线程继续执行,进程仍然存在
isAlive() 判断线程是否结束
isInterrupted() 判断线程是否中断

public static Thread currentThread() 获取当前线程对象
public static void sleep(long millis) 休眠

join

join起到的作用是等待某个线程结束
如果在main方法中用t.join() 那么主线程就会进入阻塞状态,等待t线程结束,t线程结束后在运行main方法中join之下的代码。
如果等待的线程已经结束,那么就继续执行下面的代码,就不会进入阻塞状态了
也可以传入一个参数,令主线程等待的超时时间多少ms,如:t.join(1000),那么主线程就会进入阻塞状态等待,最多等待1s。传入两个参数时,第一个参数的单位为ms,第二个参数的单位时ns。

进程和线程

进程包含线程
进程之间是相互隔离的(一个进程的结束不会影响到其它进程的正常工作)
同一个进程的线程之间,共享了一些资源,尤其是内存资源(线程与线程之间,容易互相干扰),一个线程挂了,就有可能导致整个进程都无法工作
进程是系统资源分配和管理的最小单位,线程是调度执行的最小单位

补充

线程与线程之间是共享资源的,而JVM是内存区域划分的。JVM也是一个进程(java进程)JVM在启动的时候会申请一大片内存资源,再把这些资源划分成若干份区域(方法区、堆:多个线程共享的区域。栈、程序计数器:有自己的区域。方法区和堆共享一块区域,其它的都有自己的区域)
但是如果站在操作系统的角度上来看,整个进程的虚拟地址空间,都是由若干个线程来进行共享的


通过代码感受多线程编程

public class Test 
    public static void main(String[] args) 
        // 虽然此处没有创建线程,但是还是会有一个线程来作为main方法的执行流
        System.out.println("多线程");
        while (true)
        
    

通过jconsole命令观察线程


以上是关于WEB入门浅谈20的主要内容,如果未能解决你的问题,请参考以下文章

Java入门浅谈

浅谈并小结一下web前端的知识点

移动端WEB页面

Web前端基础&JSON数据交互&表单标签库与数据绑定:大魏Java记20

浅谈多线程在java程序中的应用

浅谈前端SPA(单页面应用)