1、Java中的两种锁
1、synchronized
synchronized 是一种对象锁,它以对象为锁,在之前的笔记中有过详细解释。如果想要获得唯一的对象作为锁,有两种方法。一是在类中定义一个静态的对象,二是直接用类名.class。
2、Lock
Lock是一个接口,一般用它的实现类ReentrantLock。以下是Lock的使用示例:
Lock lock = new ReentrantLock();
lock.lock();
//中间是需要同步的代码
同步代码
//这个类用来控制线程的等待和唤醒,由对应Lock创建
Condition cd = lock.newCondition();
//这个方法类似synchronized中的wait 可以释放对应线程的锁
cd.await();
//类似synchronized中的notify 唤醒指定线程
cd.signal();
//同理 Lock也有唤醒所有线程的方法
cd.signalAll();
lock.unlock();
Lock锁的作用是lock()和unlock()之间,为了对不同线程状态进行管理,利用newCondition()生成Condition类,一个Lock对象可以实例化多个Condition对象。
2、多线程之间的协调
在之前讲到Lock类时,说到了一个Condition对象,实例化多个这种对象,可以用来对多线程进行协调,不过在此之前先看一下synchronized是如何对两个线程进行协调的。
1、synchronized实现两个线程的交替执行
public class MyMain {
public static void main(String[] args) {
Test test = new Test();
Thread th0 = new Thread(() -> {
test.test();
});
Thread th1 = new Thread(() -> {
test.test();
});
th0.start();
th1.start();
}
}
class Test {
private static int x = 1000;
private Object obj = new Object();
public void test() {
synchronized (obj) {
while (true) {
try {
//锁对象唤醒所有等待的线程
obj.notifyAll();
//线程到此等待并释放锁
obj.wait();
System.out.println(Thread.currentThread().getName() + "--" + x);
Thread.sleep(500);
x--;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
从同步代码块可以看出,拿到锁的线程先唤醒所有等待的线程,然后接下来自己进入等待状态并释放锁,等到另一个线程拿到锁唤醒它,形成线程的循环交替执行。在这个同步代码块中,第一次唤醒是没有意义的,因为锁被第一次拿到的时候,没有其它线程在等待状态,同理当一个线程被关闭,另一个线程因为没有被唤醒,造成死锁,不过这种情况下一般需要执行的业务已经结束了,所以对整个业务没有影响。
2、Lock实现三个线程顺序交替执行
package com.fan.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestMain {
public static void main(String[] args) {
LockBean lockBean = new LockBean();
ThrA thrA = new ThrA(lockBean);
ThrB thrB = new ThrB(lockBean);
ThrC thrC = new ThrC(lockBean);
thrA.start();
thrB.start();
thrC.start();
}
}
//将需要传入的参数合并为一个类,简化传参
class LockBean {
public Lock lock = new ReentrantLock();
public String flag = "A";
public Condition cd_a = lock.newCondition();
public Condition cd_b = lock.newCondition();
public Condition cd_c = lock.newCondition();
}
同时开启三个线程,分别交替输出A、B和C,这里以输出A的线程为例:
package com.fan.test;
public class ThrA extends Thread {
private LockBean lockBean;
public ThrA(LockBean lockBean) {
this.lockBean = lockBean;
}
@Override
public void run() {
while (true) {
try {
//开始加锁
lockBean.lock.lock();
//参数对象中有一个flag字符串参数,用来判断此时是否由该线程输出
if (!"A".equals(lockBean.flag)) {
//如果flag不是该线程输出语句,就让这个线程等待并释放锁
lockBean.cd_a.await();
}
System.out.println("A");
lockBean.flag = "B";
lockBean.cd_b.signal();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockBean.lock.unlock();
}
}
}
当flag不为A时,利用Condition对象使线程等待;当flag为A时,打印输出A并将flag变为下一个要输出的字符串(这里是B),然后将利用下一个线程的Condition对象将其唤醒。所以, 每次输出时,只有对应的线程在工作,并且输出完毕会更改flag,使得对应线程被唤醒,而无关线程等待。
3、锁的类型
1、公平/非公平锁
每个线程在获取锁的时候,都是随机获取的,这种锁叫非公平锁。如果按照线程等待的前后顺序获得锁,那么就是公平锁。在
Java中,synchronized一定是非公平锁,而Lock可以设置成公平锁或非公平锁。
2、自旋锁
当锁被一个线程占用的,其他线程持续不断的尝试获得锁,这种就叫自旋锁;如果是非自旋锁,那么其它线程会进入阻塞状态,
等锁被释放的时候,被唤醒,进入就绪启动状态,再去获得锁。线程在切换状态的过程中,会降低线程的执行速度;而自旋锁的
线程会一直占用内存以持续尝试获得锁。
所以,当一个锁可能会被长时间持有,那就使用非自旋锁,降低系统性能的消耗;如果锁被持有的时间不会太长的话,建议使用自旋锁,以防止线程状态切换而降低线程执行效率。
3、悲观锁和乐观锁
悲观锁就是指,持有锁的线程在执行过程中,悲观地认为一定会有其他线程干预,所以在使用共享资源前一定会先加锁,一直等
到这个线程释放锁,比如Java中的synchronized和ReentrantLock。
而乐观锁则是指持有锁的线程在执行过程中,乐观地认为不会有其它线程干预,所以不会对数据上锁,只有在更新数据的时候才
会确认数据是不是被其它线程改变了,例如Java中的原子变量类。
乐观锁适用于读比较多,写比较少的情形,这样可以减少锁的开销,而如果是相反的情况,写操作会使的锁不停地检查,而且一旦反生数据不一致,那么就会重新执行冲突数据的相关操作,所以在写比较多的时候一般使用悲观锁。一般高级语言无法直接实现乐观锁,这需要底层对CAS的支持实现。在数据库中,可以原生支持乐观锁,具体是在数据中增加一个版本号的属性。在查询数据的时候,会一并读出这个数据的版本号,当发生非查询操作时,都会检查操作数据的版本号与当前数据数据库的版本是否一致,如果相同则版本号加一,并提交更新数据,如果不相同,则认为是过期数据,不予提交本次更新操作并重试。