文章目录
线程安全性问题
在多线程环境下,有哪些不安全的因素?
- 缓存一致性问题
- 指令重排序问题
- 非原子操作问题
发生线程安全的条件?
存在两个或者两个以上的线程对同一个数据进行操作者,并且其中一定包含写操作。
不会发生线程安全问题?
- 单线程环境下——永远不会出现线程安全问题。
- 多线程环境下:操作的是不同的数据A B C
- 多线程环境下,操作的是同一个数据,但是所有的操作都是读操作。
1、缓存一致性问题
**缓存是介于物理存储与CPU处理之间的一段内存空间,主要用于存储从物理存储读出、或者要写入的数据,这需要硬件或者软件支持。**如果读取或写入物理存储中的一个字节或一段数据,如果没有缓存,那么每次的读写请求都会直接访问物理存储,而物理存储的速度一般都比较慢,而且物理定位也比较慢,缓存使用后,可以一次性读出需要的数据相邻的数据,暂时存储在缓存中,下面如果还要读取,而这部分数据已经在缓存了,就不需要再去读取物理存储,同样,如果是写操作,可以先将需要写入的数据暂时保存在缓存中,等到缓存过期或者强行清空时,再一次写入物理存储。这样可以把多次的物理存储访问,变成一次物理存储的访问,提高访问效率。
缓存的一致性就是指缓存中的数据是否和目标存储中的数据是一样的,也就是说缓存中已经修改得数据是否已经保存到了物理存储中,物理存储中已经被修改得内容,是否与缓存的内容是一样的。这就是一致性的概念。
就相当于我们启动两个线程对i进行++操作,理想情况下,结果应该是2,而现实真的是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
这样的结果为1,怎么解决呢?
1、volatile(代码层面)
因此我们要想办法让线程对共享变量的操作结果互相可见,java语言中的volatile关键字就干了一件这样的事。使用volatile修饰的共享变量,当有线程修改了他的值的时候,他会立即强制将修改的值写回到主存,并通知其他使用该共享变量的线程:他们的缓存区中关于此变量的值已经失效。请重新从主存中读取。
仔细阅读volatile干的事,一共有3点影响:
1 将修改的值强制刷新到主存
2 通知其他相关线程变量已经失效
3 其它线程再使用变量的时候就会重新从主存读取
volatile的原理探究
当第一个操作为volatile读时,无能第二个操作是什么,都不允许重排序。这个规则确保了volatile读之后的操作不能重排序到volatile读之前。
当第二个操作为volatile写时,无论第一个操作是什么,都不允许重排序。这个规则确保了volatile写之前的操作不能重排序到volatile写之后。
当第一个操作是volatile写,第二个操作是volatile读时,不允许重排序。
为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型处理器的重排序,在JMM中,内存屏障的插入策略如下:
在每个volatile写操作之前插入一个StoreStore屏障
在每个volatile写操作之后插入一个StoreLoad屏障
在每个volatile读操作之后插入一个LoadLoad屏障
在每个volatile读操作之后插入一个LoadStore屏障
StoreStore屏障可以保证在volatile写之前,前面所有的普通读写操作同步到主内存中
StoreLoad屏障可以保证防止前面的volatile写和后面有可能出现的volatile读/写进行重排序
LoadLoad屏障可以保证防止下面的普通读操作和上面的volatile读进行重排序
LoadStore屏障可以保存防止下面的普通写操作和上面的volatile读进行重排序
可以说volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性
2、加锁(硬件层面)
通过在总线加LOCK#锁的方式(硬件层面)
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
3、缓存一致性协议(硬件层面)
例如 Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的,它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
指令重排序问题
指令重排序:在不改变原本执行逻辑的基础上调整代码的位置。
编译器级别:重排序,如果代码之间没有联系顺序可以调整 (编译器开发人员提供)
指令级别:重排序,指令之间没有联系顺序调整 (JVM开发人员提供)
例:比如我们写如下代码
1.int i= 0;
2.int j = 10;
3.i++;
4.j++;
指令重排序后:
3.i++;
1.int i= 0;
2.int j = 10;
4.j++;
这样显然是没有危害,那么指令重排序会导致问题的出现吗?
例 :
线程1:
Context ctx; 当ctx变量被volatile修饰之后,重排序就一定不会出现。
boolean = flase;
1. Context ctx= new Context();
2. flag = true; 先于1执行
线程2:
3. If(flag){
4. ctx.get(); 空指针异常
}
这样就会导致ctx没有实例化,导致空指针异常。
其实我们只要和JVM的规定保持一致就可以避免了:

所以我们还是可以加volatile关键字,
只要把Context ctx; 加上关键字volatile修饰就可以。
其实指令重排序问题发生的是非常少,可遇不可求。
非原子性操作
原子(atomic)本意是"不能被进一步分割的最小粒子",而原子操作(atomic operation)
意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。
例如:i++操作为非原子操作。
通过javac反编译可以看到:++操作分为三步走
- 将index的值取出iconst-1将int类型常量压入栈中
- 执行int类型的加法
- 设置类中静态字段的值
解决方法:加锁
Synchronized(同步锁) reentrantLock(重入锁) reentrantReadWriteLock (读写锁)
所以我们再写启动10个线程对n进行1000次++的代码可以这样写
import java.util.concurrent.CountDownLatch;
class MyThread2 implements Runnable {
public static volatile int n=0;
private CountDownLatch latch ;
public MyThread2(CountDownLatch latch) {
this .latch =latch ;
}
@Override
public void run() {
synchronized (MyThread2 .class ) {
for (int i = 0; i < 1000; i++) {
n += 1;
}
}
latch .countDown() ;
}
}
public class TestDome {
public static void main(String[] args) {
CountDownLatch latch =new CountDownLatch(10);
MyThread2 runable=new MyThread2(latch ) ;
for(int i=0;i<10;i++){
Thread thread =new Thread(runable );
thread .start() ;
}
try {
latch .await() ;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MyThread2 .n);
}
}
锁实现原理及用法
synchronized实现原理
我们将被synchronized修饰的代码反编译之后如下图所示,可以看到monitorenter和monitorexit两个指令也可以使用隐式的ACC_SYNCHRONIZED和前文所述两个指令作用相同 两个指令便是synchronized底层实现的关键。
monitorenter和monitorexit显式表示:

ACC_SYNCHRONIZED隐式表示:

下面我们分别介绍monitorenter和monitorexit作用,ACC_SYNCHRONIZED与之相同就不做解释。
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象都与一个监视器锁(monitor)关联。一个monitor的lock只能被一个线程在同一时间获得。线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
a. 如果monitor的进入数为0,则意味着monitor的锁未被获得。则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是object ref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成。那么什么是monitor呢?
Monitor
什么是Monitor(监视器锁)?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。其结构如下:
Owner:
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:
关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:
表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:
用来实现重入锁的计数。
HashCode:
保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:
用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。
通过上文我们知道Monitor实际上就是Java的对象,所有的Java对象是天生的Monitor当一个线程获取到某一个对象的锁的时候不仅会对Monitor中的结构进行修改,还会修改该对象多对应的对象头中的信息进行修改。
Java对象头
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、class Pointer(类型指针)。其中class Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述。
Mark Word
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bitMark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。下图是Java对象头的存储结构(32位虚拟机):
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
相信学习到这里大家已经对mark word和monitor有了了解。synchronized的加锁底层操作就是对这两个机制的操作。但是我峨嵋你还需要清楚一个问题就是随着JDK的不断发展synchronized锁加锁方式也不断做着优化现在synchronized随着竞争越来越激烈加锁过程为:偏向锁→轻量级锁→重量级锁。

下面我们便逐个研究这几种加锁方式的底层原理:
偏向锁
锁会偏向第一次拿到锁的线程。
检测当前请求锁的线程ID是否和markword中保存的线程ID保持一致,如果一致则说明当前线程已经拿到锁了继续执行后面的代码就可以,当同一个线程重入的时候,monitor中的Nest字段计数++。
如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值
如果还未偏向,则利用CAS操作改变markword中的值来竞争锁,也即是第一次获取锁时的操作,之后markword会变换为偏向锁的状态。
如果此对象已经偏向了,并且不是偏向当前线程,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了
轻量级锁
a. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
b. 拷贝对象头中的Mark Word复制到锁记录中;
c. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
d. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。改变 monitor中的owner字段 设置成拿到锁的线程的ID;
e. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁。
f. 当同一个线程重入的时候,monitor中的Nest字段计数++
重量级锁
轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁。其加锁过程如下:
a. 使用CAS操作将 monitor中的owner字段 设置成拿到锁的线程的ID; 如果不成功则说明已经有线程获得锁,当前线程会被阻塞。
b. 对象的markword 中结构会变成重量级锁的结构,指向重量级锁的指针指向 monitor中EntryQ 所关联的互斥锁。
c. 系统级互斥锁会阻塞住没有获取到锁的线程。当同一个线程重入的时候,monitor中的Nest字段计数++
synchronized的使用
1、可以加到方法上(静态方法和普通方法都可以)
public synchronized static void add(){ //和一个对象所关联的monitor相关 静态方法获取的类的class对象monitor锁
for (int j = 0; j < 1000; j++) {
i++;
}
countDownLatch.countDown();
}
2、可以加到代码块上
public void add2(){
Object o = new Object(); //就有10分
synchronized (AddThread.class){
// 括号中对象的选择原则:会引发线程安全问题的的线程抢占的必须是同一把锁,即对象必须是同一个对象
// 如 this 或 AddThread.class
for (int j = 0; j < 1000; j++) {
i++;
}
countDownLatch.countDown();
}
}
synchronized (对象名){
……
}
这个时候需要考虑传入什么对象名?
1、Object对象o (注:不能是Integer、long等常量类)
2、this
3、AddThread.class(最佳选择)
注意事项
(1)尽量不去创建新的对象 如果能够找到现成的就用现成的就好
(2)作用域尽量小 加锁之后是让原本并发的操作编程串行。
只非原子操作的代码部分加上锁。
(3)不用使用不同的monitor锁相同的方法。
synchronized :什么时候加锁的,什么时候解锁的? 加锁和解锁是由jvm控制的。
synchronized加锁方式:阻塞的加锁方式,获取不到锁的线程会一直阻塞,并且这个阻塞状态无法中断。
ReentrantLock实现原理
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS队列,是java.util.concurrent的核心。 CAS操作AQS是基于FIFO(先入先出)队列的实现AQS中的队列是由 Node节点组成的双向链表实现的。所有的操作都是在这个AQS队列当中。如果一个线程获取锁成功那就成功了,如果失败了就将其放入等待队列当中。
加锁过程:state 初始值 0
a. 当前线程通过CAS操作来抢占锁,抢占成功则修改锁状态为1,将线程信息记录到锁当中,返回state = 1; 如果操作失败,state已经变为1
b. 否则抢占不成功
a) 获取当前锁的状态 getState 判断此时state是否位 0
b) 当前锁状态为0,表示锁空闲,没有线程获取则当前线程通过CAS操作直接获取锁,成功则将锁状态,线程信息记录,返回
ReentrantLock会将获取到锁的线程进行记录
c) 如果不为0,则获取ReentrantLock中的线程记录信息如果当前线程和获取锁的线程相同时:对锁状态state+1操作(视为重入),返回。
d) 如果判断不相同,我们就需要将当前这个线程阻塞住,通过LockSupport.park();将这个线程阻塞住。然后将这个阻塞住的线程入队。
为什么要入队? 公平锁 非公平锁。
队列:先入先出。维护公平性。
如果要实现非公平锁:
和公平锁的相同:拿不到锁的线程肯定是要入队。
不同:拿到锁的线程在它释放锁之后如果还想获取锁的话,可以直接获取,
不用再入队了,如果是公平锁这个线程则必须入队。
e)
unlock释放锁的过程:不存在竞争 解锁过程就是在对state进行减一操作,什么时候见到0
解锁成功。
a. 获取新的锁状态值 判断state的值 >0 才需要解锁。
b. 判断当前释放锁线程和锁中线程信息是否一致,不一致则抛出异常
c. 当线程信息一致时
a) 判断锁状态是否是0,即锁不在被占用,将锁中当前线程信息清除掉
b) 当锁状态不为空闲状态,对state的值进行减一操作。将最新锁状态值更新一下
解锁成功之后,如果还有其它线程想要获取锁,会让队头出队,并且结出阻塞状态
LockSupport。Unpark方法(解除阻塞状态)让这个线程继续去竞争锁。
ReentrantLock的使用
1、最简单的使用
public class ReLock {
private static ReentrantLock lock = new ReentrantLock();
public static void get() {
lock.lock();
try {
System.out.println("get");
add();
} finally {
lock.unlock();
}
}
private static void add() {
lock.lock();
try {
System.out.println("add");
} finally {
lock.unlock();
}
}
public static void main(String[] args) { //
get();
}
}
2、其他的使用
保证锁只有一个
ReentrantLock 是Lock接口下的一个类synchronized是一个关键字
类如果要使用那么就需要先创建对象,ReentrantLock有明确的加锁解锁方式,
我们加锁解锁时需要调用相应的操作。
加锁:
- lock.lock(); 阻塞的加锁方式,获取不到锁的线程会一直阻塞,并且这个阻塞状态无法中断。
- lock.tryLock() 非阻塞的加锁方式,获取不到锁的线程会得到一个Boolean类型的返回值(false),拿到返回值之后之接退出。
- tryLock(int,时间单位)可以自定义阻塞时间的加锁方式
- lock.lockInterruptibly();阻塞的加锁方式,但是时可以中断其阻塞状态的。可中断的加锁方式
解锁:
- lock.unlock(); //只有一种
读写锁
是Lock接口下的一个类
读和写线程之间也需要阻塞
读写锁的形式:读读不互斥,读写互斥,写写互斥
使用方式:也同样具有ReentrantLock的几种特殊的加锁方式,解锁方式只有一种。
使用
例:10 个线程
5个线程读 读线程之间不需要阻塞
5 个线程写 写线程之间才需要阻塞
class ReadThread implements Runnable{
int i = 0;
ReentrantReadWriteLock lock;
public ReadThread(int i, ReentrantReadWriteLock lock) {
this.i = i;
this.lock = lock;
}
@Override
public void run() {
lock.readLock().lock();//读锁的加锁方式
System.out.println(i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.readLock().unlock(); //读锁的解锁方式
}
}
}
class AddThread implements Runnable {
int i = 0;
static CountDownLatch countDownLatch;
ReentrantReadWriteLock lock;
public AddThread(CountDownLatch countDownLatch, int i, ReentrantReadWriteLock lock) {
this.i = i;
this.countDownLatch = countDownLatch;
this.lock = lock;
}
@Override
public void run() {
add2();
}
public void add2() {
for (int j = 0; j < 1000; j++) {
lock.writeLock().lock(); //写锁 加锁方式
try {
i++;
// lock.writeLock().unlock();//如果锁加上之后解不掉 会引起服务器出现死锁
} finally {
lock.writeLock().unlock();//写锁 解锁方式 为了出现异常依然能够解锁
}
}
countDownLatch.countDown();
}
}
public class TestSynchronized {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //保证读写锁只有一个
int idnex = 0;
AddThread runnable = new AddThread(countDownLatch,idnex,lock);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
ReadThread re = new ReadThread(idnex,lock);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(re);
thread.start();
}
countDownLatch.await();
System.out.println(idnex); // 执行这一行的时候并不能保证所有的子线程都执行完了
// 不加countDownLatch 结果远小于10000,加上countDownLatch 结果接近10000
// i++是非原子操作,两种方法都是有线程安全问题的
}
}
ReentrantLock和读写锁注意事项:
(1)是否有读操作 选择读写锁。
(2)只有写操作调用ReentrantLock和写锁都可以。
(3)跟据实际的需求选择最合适的加锁方式。更偏向于使用lockInterruptibly
和tryLock(int,时间单位)
(4)一定要记得将unlock方法写在finally
(5)作用域尽量小
不可重入锁与重入锁
重入锁同一个线程可以重复获取同一把锁。 学过的所有的锁都是可重入。
公平锁和非公平锁
synchronized 非公平锁
ReentrantLock和读写锁 既可以实现为公平锁也可以实现为非公平锁
/**
* 公平锁和非公平锁示例
*/
public class FairAndNonFairTest {
private static final ReentrantLock fairlock = new ReentrantLock(true);
//参数为true 锁为公平锁 为false 或者不传 非公平锁
public static void main(String[] args) {
// TODO Auto-generated method stub
FairAndNonFairTest rlt = new FairAndNonFairTest();
for (int i = 0; i < 2; i++) {
Thread fairT = new Thread(new FairAndNonFairTestThread(rlt));
fairT.setName("线程 [" + (i + 1) + "]");
fairT.start();
}
}
static class FairAndNonFairTestThread implements Runnable {
private FairAndNonFairTest rlt;
public FairAndNonFairTestThread(FairAndNonFairTest rlt) {
this.rlt = rlt;
}
public void run() {
while (true) { //重复的进行加锁和解锁
fairlock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ " 获得锁");
} finally {
fairlock.unlock();
}
}
}
}
}
synchronized和Lock接口的区别
1)Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,
若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;
3)Lock的接口下锁的加锁方式更多,synchronized只有一种阻塞加锁的方式。
4)Lock可以提高多个线程进行读操作的效率(读写锁的使用)
使用场景
ReentrantLock和读写锁使用场景更多。如果要使用一些特殊的加锁方式
只能使用ReentrantLock和读写锁
多个读操作想提高锁的性能,可以选择读写锁。
早期synchronized也被称作重量级锁,加锁的原理很重量级。
ReentrantLock和读写锁更轻量级。
jdk 1.5之前只使用ReentrantLock和读写锁
5.0之后synchronized加锁原理进行了优化,在竞争不激烈的时候synchronized的效率更高。
竞争不激烈,并且代码中没有读操作应该选择synchronized锁。
发展:synchronized更有发展前景。
用单例模式实现线程安全问题
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型
的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个
对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要
通过new方法获取创建该类对象。
单例模式的应用场景:
什么时候要使用单例模式:
(1)如果不方便管理。
(2)节约角度
如何实现单例模式:
(1)获取对象的时候不能再使用new 构造函数一定不能是公有的。
(2)类最好设计成final类型。
饿汉 懒汉模式
//评估单例模式:线程安全 高性能、懒加载
//线程安全 :发生在代码运行之前,肯定线程安全
//高性能:发生在代码运行之前 性能是高的
// 懒加载:符合懒加载的思想
//内部类和内部枚举什么时候调用什么时候队其进行编译(类加载)
public final class SingletonEnCl {
private SingletonEnCl() {
}
public enum SingletonEnum { //枚举被调用时才会发生加载 加载过程中
INSTANCE; //这个对象的初始化发生在类加载过程中
private SingletonEnCl instacne;
SingletonEnum() { //默认私有
this.instacne = new SingletonEnCl();
//INSTANCE初始化的同时创建了SingletonEnCl对象
//所以SingletonEnCl对象的创建也发生在编译期
//类加载的过程会执行几次? 所以instacne对象只有一份
}
private SingletonEnCl getInstacne() {
return instacne;
}
}
public static SingletonEnCl getSingletonEnCl() {
return SingletonEnum.INSTANCE.getInstacne();
}
public static void main(String[] args) {
SingletonEnCl singletonEnCl = SingletonEnCl.getSingletonEnCl();
}
用枚举类实现
//评估单例模式:线程安全 高性能、懒加载
//线程安全 :发生在代码运行之前,肯定线程安全
//高性能:发生在代码运行之前 性能是高的
//懒加载:不符合懒加载思想
public enum SingletonEnum {
//本身就是final 不允许被继承 final class SingletonEnum //无法通过子类间接创建对象
INSTANCE; //实例对象
SingletonEnum() { //默认私有 无法使用new
System.out.println("构造函数");
}
public void get() {
System.out.println(" ******** get ********* ");
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
for(int i = 0;i < 100;i++){
SingletonEnum instance = SingletonEnum.getInstance();
System.out.println(instance.hashCode());
}
}
}
死锁
(1)导致系统无法继续执行 (2)死锁出现的时候,产生死锁的进程或者线程不释放所持有的资源的。
死锁的出现并不意味着代码中一定使用锁了。
死锁的概念:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
客户端 服务器
**死锁是由于两个或以上的进程/线程互相持有对方需要的资源,**导致这些线程处于等待状态,无法执行。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求, 而该资源
已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持 不放。 - 不可剥夺条件:进程所获得的资源在未使用完毕之`前,不能被其他进程强行夺走, 即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
自己实现一个死锁:
启动两个线程
A和B:都是获得了1 2 两个资源之后才能继续运行下去 锁资源
控制A 先拿到1 好号资源 B 先拿到 2号资源
A 持有 : 1 B 持有:2
class DeadThread implements Runnable{
private boolean flag;
static private Object s1 = new Object();
static private Object s2 = new Object();
static private Object s3 = new Object();
public DeadThread(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (s3) {
synchronized (s1) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
System.out.println("未发生死锁");
}
}
}
}else{
synchronized (s3) {
synchronized (s2) {
try {
TimeUnit.SECONDS.sleep(5); //保证两个资源获取上是有顺序的
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
System.out.println();
System.out.println("未发生死锁");
}
}
}
}
}
}
public class DeadLock {
public static void main(String[] args) {
ReentrantLock l = new ReentrantLock();
// l.lockInterruptibly();
l.lock();
l.tryLock();
DeadThread run1 = new DeadThread(true);
DeadThread run2 = new DeadThread(false);
Thread thread1 = new Thread(run1);
Thread thread2 = new Thread(run2);
thread1.start();
thread2.start();
}
}
避免死锁的思想:
系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。 如果操作系统能保证所有进程在有限时间内得到需要的全部资源,则系统处于安全状态,否则系统是不安全的。
银行家算法:



银行家算法:寻找安全序列,如果存在安全序列则系统处于安全状态。这样就
一定不会出现死锁,如果系统处于不安全状态就有可能出现死锁。
(处于不安全状态不一定会发生死锁,但是发生了死锁就一定处于不安全状态)
死锁检测:
死锁恢复:
利用抢占恢复(将这个死锁线程所占有的资源转移给另外一个线程,
这种方式是否可行取决于资源自身性质)
利用回滚恢复(需要对进程进行备份)
通过杀死进程恢复(会有比较严重的副作用)
本文深入探讨了多线程环境下线程安全问题的根源,包括缓存一致性、指令重排序和非原子操作等问题。详细解析了volatile、synchronized、ReentrantLock等锁机制的原理与使用,以及读写锁的特性与应用场景。同时,讨论了单例模式的线程安全实现和死锁的预防与处理。
596

被折叠的 条评论
为什么被折叠?



