一、Synchronize线程同步
public class BuyController {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
new Thread(myThread1, "购票者1").start();
new Thread(myThread1, "购票者2").start();
new Thread(myThread1, "购票者3").start();
}
}
class MyThread implements Runnable {
//票数是多个线程的共享资源
private int ticket = 10;
@Override
// public void run() { //原
public synchronized void run() {
while (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩下" + ticket-- + "张票");
}
}
}
也可以这么写
class MyThread2 implements Runnable {
//票数是多个线程的共享资源
private int ticket = 10;
@Override
public void run() {
synchronized (MyThread2.class) {
while (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩下" + ticket-- + "张票");
}
}
}
}
二、各种Lock锁
Synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而Lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。
1、普通锁
/**
* 服务
*/
class MyService {
private Lock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//不管是否有异常都要释放锁
lock.unlock();
}
}
}
/**
* 线程
*/
class MyThread3 implements Runnable {
private MyService service;
public MyThread3(MyService service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
class LockTest {
public static void main(String[] args) {
MyService service = new MyService();
MyThread3 t1 = new MyThread3(service);
MyThread3 t2 = new MyThread3(service);
MyThread3 t3 = new MyThread3(service);
new Thread(t1, "窗口1").start();
new Thread(t2, "窗口2").start();
new Thread(t3, "窗口3").start();
}
}
2、公平锁与非公平锁
//公平锁
Lock nonFairLock=new ReentrantLock(true);
//默认非公平锁
Lock fairLock=new ReentrantLock(false);
重入锁
public class BuyController {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
lock.lock();
lock.unlock();
lock.unlock();
}
}
举例
class LockTest2 {
//默认是非公平锁
//static ReentrantLock lock=new ReentrantLock();
//公平锁
static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
Runnable myRunnable = new Runnable() {
@Override
public void run() {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 获得了锁对象");
} finally {
lock.unlock();
}
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(myRunnable, "线程" + i).start();
}
}
}
如果是:static ReentrantLock lock=new ReentrantLock(true); 这就是公平锁, 可以看到系统会按照线程等待的先后时间顺序,有序的为每个线程分配锁对象,这个顺序一直就是 0-2-4-3-1。
如果是:static ReentrantLock lock=new ReentrantLock(); 这默认就是非公平锁,可以看到多线程执行之后,系统更倾向于让一个之前获得锁的线程再次获得锁,这显然就体现了非公平性。
3、乐观锁与悲观锁以及CAS优化乐观锁
根据从上面的概念描述我们可以发现:
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
public class OptimisticPessimisticLock {
//悲观锁的调用方式
//synchronized
public synchronized void testMethod( ) {
//操作同步资源
}
//ReentrantLock
private ReentrantLock lock = new ReentrantLock();//需要保证多个线程使用同一个锁
public void modifyPublicResources() {
lock.lock();
//操作同步资源
lock.unlock();
}
//乐观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger();//需要保证多个线程使用同一个
public void modifyPublicResources2() {
//操作同步资源
atomicInteger.incrementAndGet();//执行自增1
}
}
我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,synchronized和Lock这种方式又有一个名字,叫做互斥锁,一次只能有一个持有锁的线程进入,再加上还有不同线程争夺锁这个机制,效率比较低,所以又称“悲观锁”。
而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
实现思想 CAS(V, A, B),V为内存地址、A为预期原值,B为新值。如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值。否则,说明已经被其他线程更新,处理器不做任何操作;无论哪种情况,它都会在 CAS 指令之前返回该位置的值。而我们可以使用自旋锁,循环CAS,重新读取该变量再尝试再次修改该变量,也可以放弃操作。
public class OptimisticPessimisticLock {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
//计数器用于阻塞
CountDownLatch countDownLatch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
//CAS执行自增
atomicInteger.incrementAndGet();
//普通的自增
count++;
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println("atomicInteger:" + atomicInteger.get());
System.out.println("count:" + count);
}
}
java语言CAS底层如何实现?
利用unsafe提供的原子性操作方法。
什么是ABA问题?怎么解决?
当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。利用版本号比较可以有效解决ABA问题。
public class MyOptimisticLock {
public Personnel findAndUpdate(List<Personnel> personnels) {
//从数据库中获取所有员工列表
List<Personnel> personnelList = findPersonnelList();
if (personnelList != null && personnelList.size() > 0) {
//获取第一个员工并修改
Personnel personnel = personnelList.get(0);
personnel.setName("Tom");
Integer result = updatePersonnel(personnel);
if (result == 1) {
return personnel;
} else {
//已被更新 则再次获取
findAndUpdate(personnelList);
}
}
return null;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Personnel {
private String name;
private Integer age;
private Integer version;
}
<update id="updatePersonnel" parameterType="com.yuange.mapper.PersonnelUpdate">
UPDATE t_personnel
SET NAME = #{name} ,
AGE = #{age} ,
VERSION = VERSION + 1
WHERE
NAME = #{name}
AND VERSION = #{version}
</update>
简要说明:表设计时,需要往表里加一个version字段。每次查询时,查出带有version的数据记录,更新数据时,判断数据库里对应id的记录的version是否和查出的version相同。若相同,则更新数据并把版本号+1;若不同,则说明,该数据发送并发,被别的线程使用了,进行递归操作,再次执行递归方法,知道成功更新数据为止。
上述findAndUpdate方法即实现了一个乐观锁,作用是冲数据库里更新一条数据病返回前端。如果并发率大,一次请求可能则会重复执行很多次findAndUpdate,则性能低。如果并发很乐观,用户请求少,则不需要用synchronized,多线程时性能高。
乐观锁适用于写比较少的情况下,即冲突比较少发生,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果经常产生冲突,乐观锁 的重复尝试 反倒会降低了性能,所以这种情况下用悲观锁就比较合适。
4、重入锁与重入自旋锁
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
/**
* @author lichangyuan
* @create 2021-10-11 14:08
*/
public class ReentryLock implements Runnable {
//synchronized版
public synchronized void get() {
System.out.println(Thread.currentThread().getId());
set();
}
public synchronized void set() {
System.out.println(Thread.currentThread().getId());
}
//ReentrantLock版
ReentrantLock lock = new ReentrantLock();
public void get2() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set2();
lock.unlock();
}
public void set2() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
get2();
}
public static void main(String[] args) {
ReentryLock reentryLock = new ReentryLock();
new Thread(reentryLock).start();
new Thread(reentryLock).start();
}
}
我们以自旋锁作为例子
class SpinLock1 {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!owner.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
- 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁
说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程) - 若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了,实际上不应释放锁。
(采用计数次进行统计)
该自旋锁即为可重入锁。
class SpinLock2 {
private AtomicReference<Thread> owner = new AtomicReference<>();
private int count = 0;
public void lock() {
Thread current = Thread.currentThread();
//如果锁的线程被锁过了则直接退出
if (current == owner.get()) {
count++;
return;
}
//如果有线程持有锁则继续回调直到锁被释放,则把当前线程锁住
while (!owner.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
//如果当前count值不为0说明前面有重入锁发生且未解锁,知道值递减为0则解锁当前线程
if (current == owner.get()) {
if (count != 0) {
count--;
} else {
owner.compareAndSet(current, null);
}
}
}
}