Java多线程与各种锁


一、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);
    }
}
  1. 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁
    说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
  2. 若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);
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

和烨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值