锁与线程安全

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的支持实现。在数据库中,可以原生支持乐观锁,具体是在数据中增加一个版本号的属性。在查询数据的时候,会一并读出这个数据的版本号,当发生非查询操作时,都会检查操作数据的版本号与当前数据数据库的版本是否一致,如果相同则版本号加一,并提交更新数据,如果不相同,则认为是过期数据,不予提交本次更新操作并重试。

你可能感兴趣的:(锁与线程安全)