Java多线程与各种锁
- 一、Synchronize线程同步
- 二、各种Lock锁
- 1、普通锁
- 2、公平锁与非公平锁
- 3、乐观锁与悲观锁以及CAS优化乐观锁
- 4、重入锁与重入自旋锁
一、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;@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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 {//悲观锁的调用方式//synchronizedpublic synchronized void testMethod( ) {//操作同步资源}//ReentrantLockprivate 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() {@Overridepublic 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 WHERENAME = #{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();}@Overridepublic 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);}}}
}