导读:记录下java中提供锁的两种方式:synchronized和Lock(juc里面提供的锁)
一、问题的产生和解决
1、产生:Java允许多线程并发控制,所以当多个线程同时操作一个共享的资源变量时,就对导致数据的不准确或者数据间相互冲突。在多线程情况下,容易对数据造成脏读,幻读及不可重读等多种并发问题。
2、解决:加入同步锁避免当前线程没有完成操作时,被其他线程调用改用。从而保证了该变量的一致性和准确性。下面说明下jdk提供的两种锁机制 synchronized和Lock(ReentrantLock(有代表性)等)
二、synchronized和lock的区别对比
一:
synchronized:
1、依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只有一个线程可以进行操作的
2、不可中断锁,适合竞争不激烈,可读性好(竞争激烈时,性能下降的快)
二:
Lock:
1、依赖特殊的CPU指令,代码实现。实现类中有代表的是ReentrantLock
2、可中断锁,多样化同步,竞争激烈时能维持常态
三:
补充Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值。
三、原子性--Synchronized
修饰代码块:大括号括起来的代码,作用于调用的对象
修饰方法:整个方法,作用于调用的对象
修饰静态方法:整个静态方法,作用于所有对象
修饰类:括号括起来的部分,作用于所有对象
(对于synchronized具体锁到什么程度,就需要根据实际场景分析。上面是4种形式,不同的方式的根本就是锁的粒度不一样。在满足条件的情况下,尽可能的将锁的范围小一点,可以加快处理速度。)
- 1、下面是相关示列代码一
@Slf4j
public class SynchronizedExample{
// 修饰一个代码块
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修饰一个方法
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
//两个不同对象同时操作,会不影响。example1和2的执行是交叉执行,不是example1执行完之后,再执行2
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
@Slf4j
public class SynchronizedExample2 {
// 修饰一个类
public static void test1(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修饰一个静态方法
public static synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
//这里会按顺序执行,因为synchronized是修饰这个类,所表现的和修饰静态方法是一样的。
SynchronizedExample2 example1 = new SynchronizedExample2();
SynchronizedExample2 example2 = new SynchronizedExample2();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
1、一个方法里面,整个一个都是一个同步代码块时,那跟修饰一个方法是一样的。
2、一个方法里面,如果所有执行代码部分都是被synchronized修饰的一个类来包围的时候,它和修饰一个静态方法它的表现是一致的。
注意一点:如果当前的类是一个父类,子类继承这个类之后,如果它想调用test2方法时是带不上synchronized这个关键字的,原因是synchronized它不属于方法声明的一部分,需要注意的。
如果子类也想使用synchronized时,则需要在方法上显示的声明synchronized关键字才行。
四、ReentrantLock与锁
ReentrantLock和Synchronized的区别
1、可重入性 (区别不大,因为它们都是同一个线程进入一次锁的计数器就自增1,所以要等到锁的计数器下降为0时,才能释放锁)
2、 锁的实现
ReentrantLock 基于 JDK 实现 (可通过源码查看如何实现)
synchronized 基于 JVM 实现
3、性能的区别
synchronized 比 ReentrantLock 差很多,但是后面synchronized 加入了 偏向锁和轻量级锁(自璇锁)后性能和ReentrantLock 差不多了。在两种方法都可用的情况下官方更建议使用synchronized,因为写法更容易。synchronized的优化感觉就是借鉴了ReentrantLock中的CAS技术,都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
4、功能区别
synchronized 的使用比较方便简洁并且是由编译器去保证锁的加锁和释放的
ReentrantLock 需要手动声明锁的加锁和释放锁,为了避免忘记手工释放锁,发生死锁,所以最好在finally中加入释放锁操作
在锁的细粒度和灵活度比较,ReentrantLock 高于 synchronized
5、ReentrantLock 独有的功能
1、可指定是公平锁还是非公平锁 (公平锁的含义:先等待的线程先获取锁)(synchronized只能是非公平锁)
2、提供了一个Condition类,可以分组唤醒需要唤醒的线程
3、提供能够中断等待锁的线程的机制,lock.lockInterruptibly()
什么时候使用ReentrantLock?就是在你需要实现这三个独有功能时就可以。其他情况下可以根据性能,业务场景时使用ReentrantLock还是synchronized。
ReentrantLock的实现是一种自璇锁,通过循环调用CAS操作来实现加锁,性能比较好是因为避免线程进入内核态的阻塞状态
- 1、示列代码
@Slf4j
public class LockExample1 {
// 请求总数
public static int clientTotal = 1000;
// 同时并发执行的线程数
public static int threadTotal = 100;
public static int count = 0;
private final static Lock lock = new ReentrantLock();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
lock.lock(); //加锁
try {
count++;
} finally {
lock.unlock(); //释放锁
}
}
}
上面这个例子就是很简单对add()操作进行加锁和释放锁,这个锁的基本操作。但是ReentrantLock中还有很多方法可以使用,可以通过查看源码了解,这也是ReentrantLock灵活性的一点,可以通过源码分析看是如何实现的。下面是有关ReentrantLock的一些方法。
@Slf4j
public class LockExample6 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
// 线程一
new Thread(() -> {
try {
reentrantLock.lock();
log.info("wait signal"); // 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal"); // 4
reentrantLock.unlock();
}).start();
//线程二
new Thread(() -> {
reentrantLock.lock();
log.info("get lock"); // 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
//condition.signal(); // 唤醒单个
log.info("send signal"); // 3
reentrantLock.unlock();
}).start();
}
}
上面标记了程序的执行顺序。从reentrantLock中取出了Condition,线程一通过调用reentrantLock.lock()方法,这个时候线程一就加入到了aqs的等待队列里面去,输出”wait signal"。一旦调用了condition.await()之后,这个线程就从aqs队列中移除了(对应的操作是锁的释放),然后马上加入到了Condition中的等待队列里去了。线程二因为线程一释放了锁被唤醒,并判断是否可以取到锁所以线程二获取到锁也加入到了aqs的等待队列中,然后输出了"get lock",接下来之后调用了condition.signalAll();(发送信号的方法),然后输出了"send signal"。这个时候Condition的等待队列里面有线程一的一个节点,于是被取出来加到了aqs的队列中去,这个时候线程一并没有被唤醒而是等到发送信号的方法结束后并调用了unlock()释放锁之后,重新唤醒继续执行。这个时候输出了"get signal",之后线程一释放锁整个线程执行完毕。
总结:整个过程中靠节点在aqs的等待队列和condition的等待队列中来回移动实现的。condition作为一个条件类很好的维护了一个等待信号的队列并在适合的时候将节点加入到aqs的等待队列中实现唤醒操作。