2019-07-31:谈一谈java线程常见的几种锁?
MoJieBlog opened this issue · comments
比如:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
- AtomicInteger
当然也可以是其他锁
4种Java线程锁(线程同步)
- synchronized
在Java中synchronized关键字被常用于维护数据一致性。
synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。
synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。
2.ReentrantLock(ReentrantLock引入两个概念:公平锁与非公平锁。)
可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。
此处AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不同。
比如:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
- AtomicInteger
当然也可以是其他锁
还有一个 unsafe 类;
cas 是乐观锁的一种实现形式, 在多核 CPU 机器上会有比较好的性能;
锁
首先,Java为什么会出现锁的概念,锁是用来干什么的?这里我给大家解释一下,锁 是保证 线程同步与安全的.
synchronized
synchronized 有什么用?大家看一下这段代码
public class Synchronized2Demo implements TestDemo {
private int x = 0;
private void count() {
x++;
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
count();
}
System.out.println("final x from 1: " + x);
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
count();
}
System.out.println("final x from 2: " + x);
}
}.start();
}
}
synchronized的互斥访问
当添加 synchronized 的时候,多个线程不能同时访问同一个方法,synchronized 的意义是保证方法或代码块的内部资源的互斥访问,同一时间用用一个monitor监视代码最多一个代码访问,怎么解释这句话呢?
运行上段代码,我们不难发现:
final x from 1: 1000000
final x from 2: 1314570
Fuck! 竟然结果不是200 0000,而且每次结果都不一样,这是为什么呢?
因为:x++ 不是原子操作, 不是一行代码执行的,他是可以分解的代码,可以分离成这样
int temp = i;
i + 1 = temp;
你看,这是两段代码吧,加 synchronized 修饰 就可以完美解决这个问题.
synchronized 的本质:
-
保证方法的内部或内部代码块内部资源(数据)的互斥访问,即同一时间,由同一个 Monitor 监视代码,最多只能一个线程在访问
-
保证线程之间对监视资源的数据同步.即,任何线程获取 Monitor 后第一时间,会将共享数据的缓存复制到自己的缓存中;任何线程在释放 Monitor 的第一时间,会将缓存的数据复制到共享内存中
Lock / ReentrantReadWriteLock
同样是加锁机制 ,使用方式更加灵活,同时也更加麻烦一些
Lock lock = new ReentrantLock();
{
// -------伪代码-------
}
lock.lock(); try {
x++;
} finally {
lock.unlock();
}
finally 作用是保证在方法提前结束或出现Exception的时候,依然能正常释放锁.
一般不会直接使用 Lock,而是直接使用更加复杂的锁,如: ReadWriteLock:
public class ReadWriteLockDemo implements TestDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 0;
@Override
public void runTest() {
print(15);
count();
}
public void count() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
public void print(int time) {
readLock.lock();
try {
for (int i = 0; i < time; i++) {
System.out.println(x + "");
}
System.out.println("over");
} finally {
readLock.unlock();
}
}
}
volatile
- 保证加了 volatile 关键字的字段的操作具有同步性,以及对 long 和 double 操作的原子性.因此 volatile 可以看做简化版的 synchronized
- volatile 只对基本数据类型(byte ,short ,long ,char,int ,float ,double,boolean)的赋值操作和对象引用赋值有效,你修改 User.name 是不能保证同步的
- volatile 依然解决 不了 ++ 的原子性问题
java.util.concurrent.atomic
- AtomicInteger , AtomicInteger等类,作用和 volatile 基本一致,可以看做通用版本的 volatile
AtomicInteger atomicInteger = new AtomicInteger(0);
{
// -------伪代码--------
}
atomicInteger.getAndIncrement();
解决了 ++ 原子性 问题,非常好用.
悲观锁
不管数据怎么样我都会加锁,这个就是悲观锁
乐观锁
我认为,别人不会读数据我倾向于别人不会动数据,所以不对代码块加锁,这就是乐观锁
线程安全问题的本质:
在多线程访问共同资源时,在某一个线程对资源进行写操作中途(写入已经开始了,但还没结束),其他线程对这个写了一半资源进行读操作,或基于这个写了一半的资源进行了写操作,导致出现数据错误
锁机制的本质:
通过共享资源进行访问限制,让同一时间只有一个线程可以访问资源,保证数据的准确性
总结:
无论是线程安全问题,还是针对线程衍生的锁机制,它的核心在于共享资源,而不是某个方法或几行代码
当然还有死锁问题,这里我就不一一和大家阐述了.
最后大家觉的我的内容对大家有帮助,欢迎通过以下渠道阅读我的文章或github star +1
github | CSDN | 掘金 |
---|---|---|
MicroKibaco | Kibaco | 小木箱 |