
线程定义比较抽象,简单的说就是一个代码执行流。许多执行流可以混合在一起由CPU调度。线程是允许各种任务交互执行的方式。
Java的线程在 *** 作系统的实现模式依系统不同而不同,可能是系统级别的进程或线程,但对于程序员来说并没有影响。
任务交互的一个好处是增加程序响应。如一个界面程序执行一段耗时的数据库查询,使用单独的线程可以让界面依然响应用户的其他输入,而单线程只能等待查询结束再处理。
JVM以及 *** 作系统会优先处理优先级别高的线程,但不代表这些线程一定会先完成。设定优先级只能建议系统更快的处理,而不能强制。
另外,在运行时,并没有按照函数分界,而是按照机器码/汇编码分界。也就是说不保证任何一段代码是被完整而不打断的执行的(除非你已经使用同步手段)。正由于如此,各种线程同步的方法应运而生。
在分布式系统中,为了保证对数据的修改有最终一致性,通常使用分布式锁或者分布式事务。比如常见的多个系统同时修改商品,既依赖于现有数据也要修改数据,如果没有限制,高并发情况下很可能最终数据是错误的。
与单机锁不同,分布式锁更加复杂,需要考虑网络延迟、服务阻塞等,通常具有如下特点:
利用数据库主键唯一的特性,可以基于唯一主键保证多次 *** 作只有一次成功。在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。释放锁时,直接删除数据库记录即可。
此方案存在的问题是强依赖数据库,容易形成热点,数据库锁表导致的超时会影响性能,或者数据库宕机会导致服务不可用。并且,数据库本身没有失效机制,如果任务崩溃会导致数据库中的锁不能被释放。数据库插入 *** 作本身没有阻塞机制,故无法实现分布式锁的阻塞等待,任务线程可能需要重复尝试插入。由于唯一主键的存在,持有锁的线程也无法重复获得锁,其他线程竞争锁的过程中也无法根据优先级进行分配。
在数据库中为表增加一个版本号字段,每次 *** 作时判断版本号,只有版本号一致才能进行对应的修改,修改后版本号加 1,通过 CAS 的方式进行修改。
此实现会增加数据库 *** 作的次数,高并发情况下可能性能不好。
for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。我们可以认为获得排他锁的线程即获得分布式锁,任务执行完成后通过 commit 来释放锁。for update 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
但是 MySQL 会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。
setnx 的含义就是 SET if Not Exists,主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。setnx 命令不能设置 key 的超时时间,只能通过 expire() 来设置。
锁的实现步骤:
这个方案如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题。
这个方案是对上一个方案的优化版本。
getset() 命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么首次执行的返回值是 null。
锁的实现步骤:
这个方案在任务处理超时或发生宕机时,无需担心锁超时问题,下次请求可以判断出实际上锁已经超时了。
zookeeper 由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。
zookeeper 数据是目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
子节点有三种类型。
zookeeper 提供了 Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。
可以利用临时节点与 watch 机制实现分布式锁。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除 *** 作后再去争锁。临时节点好处在于当进程挂掉后锁的节点自动删除不会发生死锁。
缺点在于所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。
一个可行的优化方案是上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。watch 事件到来后,再次判断是否序号最小。取锁成功则执行代码,最后释放锁(删除该节点)。
性能上可能没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。zookeeper 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。
分布式锁比较复杂,也比较容易发生死锁。目前主流的实现方式包括:
分布式锁及其常见实现方式 - 程序之心
可以使用线程类中的join方法,在一个线程中启动另一个线程,另外一个线程执行完该线程继续执行。T3的run方法中执行t2join(),T2的run方法中执行t1join(),这样就会按照T1,T2,T3的顺序执行了。
AQS是javautilconcurrent包下的工具类,全称是AbstractQueuedSynchronizer抽象队列同步器,AQS是多线程同步器,Lock、CountDownLatch、Semaphore都用到了AQS,从本质上来说AQS提供了两种锁机制,分别是排它锁和共享锁。
排它锁:就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如Lock中的ReentrantLock重入锁实现就是用到了AQS中的排它锁功能。
共享锁:也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch、Semaphore都是用到了AQS中共享锁功能。
CountDownLatch是基于执行时间的同步类,允许一个或多个线程等待其他线程完成 *** 作,构造方法接收一个int参数作为计数器,如果要等待n个点就传入n,每次调用countDown方法时计数器减1,await方法阻塞当前线程直到计数器变为0,由于countDown方法可用在任何地方,所以n个点既可以是n个线程,也可以是一个线程里的n个执行步骤。
循环屏障是基于同步到达某个点的信号量触发机制,作 是让 组线程到达 个屏障时被阻塞,直到最后 个线程到达屏障才会解除。构造 法中的参数表示拦截线程数量,每个线程调 await 法告诉CyclicBarrier 已到达屏障,然后被阻塞。还 持在构造 法中传 个 Runnable 任务,当线程到达屏障时会优先执 该任务。适 于多线程计算数据,最后合并计算结果的应 场景。CountDownLacth 的计数器只能 次, CyclicBarrier 的计数器可使 reset 法重置,所以CyclicBarrier 能处理更为复杂的业务场景,例如计算错误时可 重置计数器重新计算。
信号量 来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理使 公共资源。信号量可以 于流量控制,特别是公共资源有限的应 场景, 如数据库连接。Semaphore 的构造 法参数接收 个 int 值,表示可 的许可数量即最 并发数。使 acquire 法获得 个许可证,使 release 法归还许可,还可以 tryAcquire 尝试获得许可。
synchronized是重量级锁,拿到锁必须要阻塞,当量大的时候会一直阻塞唤醒,优化一下,不要让其阻塞,只是告诉有这么个标记,在synchronized的边界做循环,这就是自旋,如果做了多次循环发现还没有获得锁,再阻塞。
有一个线程来访问代码块,没有锁的竞争,偏向某个线程,把偏向锁的偏向标记存储为线程的线程id(主要是同一个线程反复抢占锁的场景),只有一个线程,如果线程抢占那么就会升级成轻量级锁,偏向锁默认是关闭的。
有线程竞争,自旋去判断这个对象的锁是否释放,自旋次数(默认根据上一次自旋次数和锁的释放时间来决定),会升级重量级锁
线程阻塞等待。
首先synchronized会尝试使用偏向锁的方式去竞争锁资源,如果能够竞争到偏向锁,表示加锁成功直接返回。如果竞争锁失败,说明当前锁已经偏向了其他线程,需要将锁升级到轻量级锁,在轻量级锁状态下,竞争锁的线程根据自适应自旋次数去抢占锁资源。如果在轻量级锁状态下还是没有竞争到锁,就只能升级到重量级锁,在重量级锁状态下,没有竞争到锁的线程就会被阻塞,线程的状态就是Blocked。
在运行的某个函数或者代码,因为抢占资源或者中断等原因导致函数或者代码的运行中断,等待中断程序执行结束后,重新进入这个函数或者代码中运行,并且运行结果不会受到影响,那么这个函数或者代码就是可重入的。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义。线程A在第一次抢占到锁,在还没有释放之前再次得到锁,这个时候就不需要重新抢占锁,而是增加重入次数,然后锁需要被释放两次才能获得真正的释放。
ReentrantLock是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题
特性:
1)支持可重入。
2)支持公平和非公平。
3)提供了阻塞竞争锁和非阻塞竞争锁的两种方法,分别是lock()和tryLock()。
几个非常关键的技术:
锁的竞争,ReentrantLock通过互斥变量,使用CAS机制来实现的,没有竞争到锁的线程,使用了AbstractQueuedSynchronized这样一个队列同步器来存储,底层是通过双向链表来实现的,当锁被释放之后,会从AQS队列里的头部唤醒下一个等待锁的线程。
公平和非公平的特性,主要体现在竞争锁的时候,是否需要判断AQS队列存在等待中的线程。等待队列里的锁则是公平锁,如果都可以竞争锁,则是非公平锁。
锁的重入特性,在AQS里面有一个成员变量来保存当前获得锁的线程,当同一个线程下次再来竞争锁的时候,就不会去走锁竞争的逻辑,而是直接增加重入次数。
对于并发间 *** 作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较替换这两个动作作为一个原子 *** 作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
对于并发间 *** 作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行 *** 作时,都会持有一个独占的锁,就像synchronized,直接上锁 *** 作资源。
择一良人,选一城市,三餐四季,春夏秋冬
以上就是关于java 多线程是什么全部的内容,包括:java 多线程是什么、分布式锁及其常见实现方式、每日一练进击大厂「DAY7」并发编程4等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)