java 锁的实现

自旋锁

在使用wait()和notify()来控制代码的执行时,可能会出现假唤醒的情况。

public class Demo{

    List<String> list = new ArrayList<>();

    public void push(String val){
        synchronized (this){
            list.add(val);
            notify();
        }
    }

    public void pop() throws InterruptedException {
        synchronized (this){
            if(list.size()<=0){
                wait();
            }
            list.remove(list.size()-1);
        }
    }
}

若有线程A、B、C

  1. 线程A首先进入pop()方法时,list的size()<=0,线程A阻塞。
  2. 线程B调用push()方法,list.size()=1,并且唤醒线程A的pop方法。
  3. 在线程A被唤醒执行后面的程序之前,线程C先调用pop()方法,此时的list.size()为1,则不用进入if判断,直接调用remove()方法,list.size()为0。
  4. 然后线程A直接调用remove()方法,但是此时的list.size()为0,程序报错。

为了解决假唤醒,需要将if改为while。这样的一个while循环叫做“自旋锁”

自旋锁是一种无锁状态,是通过不断比较来确保当前值没有被改变。本身cas的内部就是自旋锁。此处的demo并没有解决aba的问题。

ThreadLoacl

当多线程访问的同一个对象时,该对象的成员变量为所有线程共有。当某一个线程修改对象的成员变量时,会对其他线程造成影响。ThreadLocal类用来保证创建的变量在每个线程中独立存在,多个线程间互不影响。虽然不同的线程执行同一段代码时,访问同一个ThreadLocal变量,但是每个线程只能看到私有的ThreadLocal实例。所以不同的线程在给ThreadLocal对象设置不同的值时,他们也不能看到彼此的修改。

  • 使用map来保存变量
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T(3);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

class T  implements Runnable{
    Map<String,Integer> map = new HashMap<>();
    int i;
    T(int i){
        this.i = i;
    }
    @Override
    public synchronized void run() {
        System.out.println(map.get("val"));
        map.put("val",i);
    }
}

输出结果:t1线程输出为null,而t2线程输出为3
此时mapt1线程执行时被修改了,所以在t2线程访问map时,map中是有值的。

  • 使用ThreadLocal类来保存的变量
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T(3);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

class T  implements Runnable{
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    int i;
    T(int i){
        this.i = i;
    }
    @Override
    public synchronized void run() {
        System.out.println(threadLocal.get());
        threadLocal.set(i);
    }
}

输出结果:t1线程输出为null,t2线程输出也为null
由于ThreadLoacl类中保存的变量为每个线程私有的,所以在t1线程中将ThreadLocal中设置值后,t2线程ThreadLocal时仍为null

初始化ThreadLocal
重新ThreadLocal中的initialValue() 方法,就可以为ThreadLocal对象指定一个初始化值,并对所有的线程都可见。

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T(3);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

class T  implements Runnable{
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
    	//初始化ThreaLocal
        public Integer initialValue() {
            return 0;
        }
    };
    int i;
    T(int i){
        this.i = i;
    }
    @Override
    public synchronized void run() {
        System.out.println(threadLocal.get());
        threadLocal.set(i);
    }
}

输出结果:t1线程输出为0,t2线程输出也为0。

死锁

有A、B两线程,线程A在持有锁的同时想要获取线程B的锁。而此时线程B也同时持有锁并想要获取线程A的锁,这样的情况会导致两线程一直阻塞,造成死锁

public class Demo3 {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        T ta = new T(a,b);
        T tb = new T(b,a);
        //线程持有a的锁并想要获取b的锁
        new Thread(()->{ta.test();}).start();
        //线程持有b的锁并想要获取a的锁
        new Thread(()->{tb.test();}).start();
    }
}

class T{
    Object a;
    Object b;

    T(Object a, Object b){
        this.a = a;
        this.b = b;
    }

    public void test()  {
        synchronized (a){
            try {
            	System.out.println("~~~");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b){
				System.out.println("...");
            }
        }
    }
}

使用jps查看java进程的情况如下:

14692 Demo3
1432 RemoteMavenServer
72 Launcher
10908 Jps
8716

使用jstack查看某个Java进程内的线程堆栈信息(这里要看Demo3线程情况,所以输入的是jstack 14692)。可以看出显示当前Thread-1Thread-0为阻塞状态。

"Thread-1" #12 prio=5 os_prio=0 tid=0x0000000019872000 nid=0x36bc waiting for monitor entry [0x000000001a2af000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at t01.T.test(Demo3.java:32)
        - waiting to lock <0x00000000d5ebfe70> (a java.lang.Object)
        - locked <0x00000000d5ebfe80> (a java.lang.Object)
        at t01.Demo3.lambda$main$1(Demo3.java:10)
        at t01.Demo3$$Lambda$2/1096979270.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #11 prio=5 os_prio=0 tid=0x0000000019888000 nid=0x3334 waiting for monitor entry [0x000000001a1af000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at t01.T.test(Demo3.java:32)
        - waiting to lock <0x00000000d5ebfe80> (a java.lang.Object)
        - locked <0x00000000d5ebfe70> (a java.lang.Object)
        at t01.Demo3.lambda$main$0(Demo3.java:9)
        at t01.Demo3$$Lambda$1/1324119927.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

嵌套管程锁死

线程1获取A锁后又获取了B锁,却调用了B.wait()的方法去释放B锁,导致程序阻塞,无法释放A锁,需要线程2去唤醒。但线程2需要持有A锁,最终程序无法运行。

class MyLock{

   private Object o = new Object();
   private boolean isLock = false;

   public synchronized void lock() throws InterruptedException {
       while(isLock){
           synchronized (o){
               o.wait();
           }
       }
       isLock = true;
   }

   public synchronized void unLock(){
       isLock = false;
       o.notify();
   }
}

第一个线程调用lock()方法,在调用unLock()方法前,第二个线程调用了lock()方法,此时第二个线程处于等待状态。此时当一个线程准备调用unLock()方法时去唤醒第二个线程时,却无法获取当前对象的锁,导致程序阻塞。

公平锁

  当有多个线程需要获取同一把锁的时候,若其中一个线程获取,则其他线程被阻塞,等待锁的释放。当锁释放时,所有线程重新抢占这把锁,这样可能会出现一种情况,有一个线程总是无法获取到锁,导致线程处于饥饿状态。
  java中的synchronized是无法保证线程的公平性。java本身是无法实现百分百线程的公平性,但是可以提高线程的公平性。

  • 先实现一个普通的锁
public class Lock{

   private boolean isLock = false;

   private Thread thread = null;

   public synchronized void lock() throws InterruptedException {
       //自旋锁的写法
       while(isLock){
           System.out.println(Thread.currentThread().getName()+"等待");
           this.wait();
       }
       isLock = true;
       thread  = Thread.currentThread();
   }

   public synchronized void unLock(){
       System.out.println("唤醒");
       //防止同一线程多次调用unLock方法唤醒其他线程或是其他
       if(Thread.currentThread() != thread){
           throw new IllegalMonitorStateException();
       }
       isLock = false;
       thread = null;
       this.notify();
   }
}

当第一个线程调用lock() 方法时会将isLock 置为true,后续线程调用lock() 方法时,会进入while 判段,调用wait() 方法,释放锁。这样之后所有的线程都可以进入lock() 方法,然后在调用wait() 方法时阻塞。在第一个线程执行unlock() 方法后会唤醒其中的一个线程。

  • 出现嵌套管程锁死的公平锁
public class FairLock {
    private List<QueueThread> list = new ArrayList<>();
    private Thread lockThread = null;
    private boolean isLock = false;

    public void lock() throws InterruptedException {
        QueueThread queueThread = new QueueThread();
        synchronized (this){
            list.add(queueThread);
            while(isLock || queueThread != list.get(0)){
            	//这个synchronized(queueThread)加不加都一样,因为本身doWait()方法已经是一个同步方法,而且锁的时当前对象。这样是为了代码看起来更清晰
            	synchronized(queueThread){
					queueThread.doWait();
				}
            }
            isLock = true;
            lockThread = Thread.currentThread();
            list.remove(list.get(0));
        }
    }

    public synchronized void unLock(){
        if(Thread.currentThread() != lockThread){
            throw new RuntimeException();
        }
        lockThread = null;
        isLock = false;
        if(list.size()>0){
        	//这个同步代码块也是为了代码更清晰
        	synchronized(list.get(0)){
        		list.get(0).doNotify();
			}
        }
    }
}

class QueueThread{

    public synchronized void doWait() throws InterruptedException {
        this.wait();
    }

    public synchronized void doNotify(){
        this.notify();
    }
}

这个公平锁会出现嵌套管程锁死,当调用queueThread.doWait();方法时虽然会释放queueThread对象的锁,但是无法释放this这个锁,导致doNotify()方法无法获取锁,程序被阻塞。

  • 将嵌套的锁提取出来
public class FairLock {

    private List<QueueThread> list = new ArrayList<>();
    private boolean isLock = false;
    private Thread lockThread = null;

    public void lock() throws InterruptedException {
        QueueThread queueThread = new QueueThread();
        synchronized(this){
            list.add(queueThread);
        }
        while(isLock || queueThread != list.get(0)){
            synchronized (queueThread){
                queueThread.doWait();
            }
        }
        synchronized (this){
            isLock = true;
            lockThread = Thread.currentThread();
            list.remove(list.get(0));
        }
    }

    public synchronized void unLock(){
        if(Thread.currentThread() != lockThread){
            throw new RuntimeException();
        }
        isLock = false;
        if(list.size()>0){
            list.get(0).doNotify();
        }
    }
}

class QueueThread{

    public synchronized void doWait() throws InterruptedException {
        this.wait();
    }

    public synchronized void doNotify(){
        this.notify();
    }
}

这样的锁还是有一个缺点。当线程1调用lock()方法,线程2在执行完第一个同步代码块,进入while语句中的第二个同步代码块时,由于一个同步代码块执行完后,当前锁被释放,这时的线程1在线程2执行doWait()前执行了unLock()方法,这时的线程2再执行doWait()方法时会被阻塞,但是由于unLock()方法被调用了,所以就无法唤醒线程2的等待。
为了解决这个问题,需要在 QueueThread类中保存信号

  • 最终的公平锁
public class FairLock {

    private List<QueueThread> list = new ArrayList<>();
    private boolean isLock = false;
    private Thread lockThread = null;

    public void lock() throws InterruptedException {
        QueueThread queueThread = new QueueThread();
        synchronized(this){
            list.add(queueThread);
        }
        while(isLock || queueThread != list.get(0)){
            synchronized (queueThread){
                queueThread.doWait();
            }
        }
        synchronized (this){
            isLock = true;
            lockThread = Thread.currentThread();
            list.remove(list.get(0));
        }
    }

    public synchronized void unLock(){
        if(Thread.currentThread() != lockThread){
            throw new RuntimeException();
        }
        isLock = false;
        if(list.size()>0){
            list.get(0).doNotify();
        }
    }
}

class QueueThread{
    private boolean isLock = false;

    public synchronized void doWait() throws InterruptedException {
        while(isLock){
            this.wait();
        }
        isLock = true;
    }

    public synchronized void doNotify(){
        isLock = false;
        this.notify();
    }
}

可重入锁

一个线程在持有一个锁后,那么这个线程则可以进入其他持有这个锁的代码块。这就是可重入锁。例如:

class Test{
    public synchronized  void outer(){
        inner();
    }

    public synchronized void inner() {
    }
}
  • 之前写过的一个普通的锁
public class Lock{

   private boolean isLock = false;

   private Thread thread = null;

   public synchronized void lock() throws InterruptedException {
       //自旋锁的写法
       while(isLock){
           this.wait();
       }
       isLock = true;
       thread  = Thread.currentThread();
   }

   public synchronized void unLock(){
       //防止同一线程多次调用unLock方法唤醒其他线程或是其他
       if(Thread.currentThread() != thread){
           throw new IllegalMonitorStateException();
       }
       isLock = false;
       thread = null;
       this.notify();
   }
}

这种锁是不可重入的,但一个线程重复调用了lock()方法时,后面的线程会被阻塞。

  • 将这个锁改为可重入锁
public class Lock {

    private boolean isLock = false;
    private Thread thread;
    private int current = 0;

    public synchronized void lock() throws InterruptedException {
    	//使一个线程可以重复加锁
        while(isLock && thread != Thread.currentThread()){
            this.wait();
        }
        isLock = true;
        current++;
        thread = Thread.currentThread();
    }

    public synchronized  void unLock(){
        if(thread != Thread.currentThread()){
            throw new RuntimeException();
        }
        current--;
        if(current == 0){
            isLock = false;
            thread = null;
            this.notify();
        }
    }
}

读/写锁

当一个线程持有读锁时,这个线程无法再去获取写锁,但是可以获取读锁。
当一个线程持有写锁时,这个线程无法去获取写锁,也无法再获取读锁。

  • 一个不可重入的读写锁
public class ReadWriterLock {

    private boolean isReadLock = false;
    private boolean isWriterLock = false;
    private Thread writerThread = null;
    private int readCount = 0;

    public synchronized void readLock() throws InterruptedException {
        while(isWriterLock){
            wait();
        }
        readCount++;
        isReadLock = true;
    }

    public synchronized void unReadLock() throws InterruptedException {
        readCount--;
        if(readCount == 0){
            isReadLock = false;
            notifyAll();
        }
    }

    public synchronized void writerLock() throws InterruptedException {
        while(isReadLock || isWriterLock){
            wait();
        }
        isWriterLock = true;
        writerThread = Thread.currentThread();
    }

    public synchronized void unWriterLock(){
        if(writerThread != Thread.currentThread()){
            throw new RuntimeException();
        }
        isWriterLock = false;
        notifyAll();
    }
}

信号量

信号量是用来控制并发的数量,访问线程若超过所设置的信号量,则会被阻塞。

  • 一个简单信号量的实现
public class Semaphore {
    //当前信号量
    private int singles = 0;
    //最大信号量
    private int maxSingles = 10;

    public Semaphore(){}

    public Semaphore(int maxSingles){
        this.maxSingles = maxSingles;
    }

    //获取信号
    public synchronized void acquire() throws InterruptedException {
        while(singles == maxSingles){
            wait();
        }
        singles++;
        notify();
    }

    //释放信号
    public synchronized void release() throws InterruptedException {
        while (singles == 0){
            wait();
        }
        singles--;
        notify();
    }
}

乐观锁

乐观锁本身是非阻塞的,可以通过非阻塞算法来保证一致性。

class Demo{

    AtomicInteger count = new AtomicInteger();

    public void add(){
        boolean updated  = false;
        while(!updated){
            int temp = count.get();
            //如果更新失败则返回false,则会再次执行compareAndSet方法
            updated  = count.compareAndSet(temp,temp+1);
        }
    }
    
    public int get() {
        return count.get();
    }
}

compareAndSet方法会先去比较当前的值被是否改变,如果没有被改变则更新,否则不进行任何操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值