1. 线程创建的方式
多线程实现方式
方式一:extends Thread
方式二: implements Runnable(没有返回值)或 implements Callable(有返回值)
当我们调用start()启动线程时,底层虚拟机会自动调用run()执行我们的业务
2. 线程的状态
线程的几种状态和阻塞原因
线程生命周期,主要有 五 种状态:
1. 新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
2. 就绪状态(Runnable): 当调用线程对象的start()方法,线程即为进入就绪状态.
处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行
3. 运行状态(Running): 当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态
4. 阻塞状态(Blocked): 处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
等待阻塞: 运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
其他阻塞: 调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
5.死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期
3. 并行与并发
并发(concurrency)和并行(parallellism)是:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。
4. Synchronized与Lock的区别
锁升级的时候,需要先撤销回无锁状态,然后再升级锁
4.1 存在层次上
synchronized: Java的关键字,在jvm层面上
Lock: 是一个接口
4.2 锁的获取
==synchronized: == 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
4.3 锁的释放(死锁产生)
synchronized: 在发生异常时候会自动释放
占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放
占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
4.4 锁的状态
synchronized: 无法判断
Lock: 可以判断
4.5 锁的类型
synchronized: 可重入 不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可)
4.6 性能
synchronized: 少量同步
Lock: 大量同步
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,
但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
4.7 调度
synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制
==Lock: == 可以使用Condition进行线程之间的调度
4.8 用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
4.9 底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
5. sleep、wait、join、yield
5.1 sleep
在指定时间内让当前正在执行的线程暂停执行,但不会释放锁
5.2 wait
线程等待,只有获得了锁才能调用,要不然就是无效监听,只能在synchronize里边使用,会释放锁
唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。
wait()和notify()只能在synchronize里边使用。
5.3 join
线程协同调用,等待该线程终止.
如果在a线程里边调用了线程b的join方法,那么会等到b线程执行完以后 才会继续执行a
5.4 yield
可以使线程从运行状态切换就绪状态 而不是阻塞状态 ,
不能保证目前正在运行的线程迅速切换就绪状态 ,
即使迅速切换为就绪状态 系统通过线程调度机从所有就绪线程里挑选下一个执行线程时 就绪的线程可能被选中 也可能不被选中 其调度过程受其他因素的影响(如 优先级)
yield()只能使同优先级或更高优先级的线程有执行的机会
5. 乐观锁和悲观锁
5.1 乐观锁
5.1.1 定义
乐观锁在操作数据时非常乐观,认为别的线程不会同时修改数据,
所以不会上锁,但是在更新的时候会判断一下在此期间别的线程有没有更新过这个数据
5.1.2 大体流程
- 两个线程,如线程A、线程B直接获取同步资源数据,不会加
锁,执行各白的操作 - 线程A、B在更新同步资源之前,都会先判断资源是否被其他线程修改
- 如果同步资源没有被其他线程修改,那么直接更新内存中同步
资源的值 - 如果同步资源被其他线程修改了,那么根据需要执行不同的操
作,直接报错或者重试
5.1.3 实现
5.1.3.1 CAS
例如Java 中java.util.concurrent.atomic包下面的原子变量使用了
乐观锁的一种 CAS 实现方式
5.1.3.2 version版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被 修改的次数。当数据被修改时,version 值会 +1
。当线程 A 要更
新数据时,在读取数据的同时也会读取 version 值,在提交更新
时,若刚才读取到的 version 值与当前数据库中的 version 值相等
时才更新,否则重试更新操作,直到更新成功
update user set name = "zs", version = oldVersion + 1 where
version = oldVersion
5.1.4 总结
乐观锁适合读操作多的场景,不加锁的特点能使其读操作的性能大
幅提升
5.2 悲观锁
5.2.1 定义
悲观锁在操作数据时比较悲观,每次去拿数据的时候认为别的线程
也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的
线程想拿这个数据就会阻塞直到它拿到锁
5.2.2 大体流程
1、多个线程,如线程A,B尝试获取同步锁
2、假设线程A先加锁成功并执行对应的操作,那么线程B只能等待
线程A释放锁之后才能操作,线程B处于阻塞状态
3、线程A释放同步锁,然后CPU会唤醒等待的线程,即线程B会面
次尝试获取锁
4、线程B成功获取锁,再执行自己的操作
5.2.3 实现
Java中 synchronized 和 Reentrantlock 等独占锁
5.2.4 缺点
需要阻塞,导致效率低下
可能造成某个线程永久等待,即发生死锁的可能性比较大
5.2.5 总结
悲观锁适合并发写入操作多的场景,先加锁再进行写操作,能保证
写操作的数据正确性
5.3 ABA
5.3.1 本质
ABA问题的根本在于CAS在修改变量的时候,无法记录变量的状态,比如修改的次数,是否修改过这个变量。
当执行campare and swap会出现失败的情况。例如,一个线程先读取共享内存数据值A,随后因某种原因,线程暂时挂起,同时另一个线程临时将共享内存数据值先改为B,随后又改回为A。随后挂起线程恢复,并通过CAS比较,最终比较结果将会无变化。这样会通过检查,这就是ABA问题。 在CAS比较前会读取原始数据,随后进行原子CAS操作。这个间隙之间由于并发操作,最终可能会带来问题。
5.3.2 解决方案
5.3.2.1 AtomicStampReference
类路径: java.util.concurrent.atomic
AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
public V getRerference();
public int getStamp();
public void set(V newReference,int newStamp);
5.3.2.2 AtomicStampReference的使用实例
我们定义了一个money值为19,然后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功,这就保证了每次只会被执行一次。