Java基础-锁

    为了提高系统的资源利用率,促使了进程,线程的出现。进程和线程提高了系统CPU利用率的同时,又引出了一些其他的问题。
    这里仅讨论线程安全性的问题,因为多个线程中操作执行顺序是不可预测的,甚至会产生一些奇怪的结果。


多线程造成的安全性问题

下面通过几个简单的示例来看一下,在没有锁的情况下会存在什么问题。
1、错误的单例模式

public class SingleInstance {
    // 实例对象
    private static Object instance;
    
    /**
     * 获取该对象的实例
     */
    public Object getInstance() {
        if (instance == null) {
           instance = new Object();
        }
        return instance;
    }
}

    该类提供的方法本意是,多次请求getInstance()方法,instance对象只初始化一次。在单线程的情况下貌似没什么问题,但是在多线程的情况下,就会存在问题了。
    假定线程A和线程B同时调用getInstance方法,A线程判断instance对象当前为null,实例化了一个对象。在A线程没有实例化完之前,执行了线程调度,B线程也访问当了if的判断条件中,发现instance也为null,也实例化了一个对象并赋值给了instance。最终结果就是A,B线程拿到了不同的对象实例。
2、共享变量被访问

public class Counter{
   private integer count = 0;
    
   public static Integer addCount() {
        return count++;
   }
}

    上述是一个技术器类,它提供了统计每个线程访问服务器次数的作用。但是当多个线程同时并发访问,它的统计出来的数据就会存在误差。为什么会有这个问题呢?
    count++并不是一个原子操作,它主要包含这几步,读取count变量在内存中的值,将count值加一,得到的结果再赋值给count变量,这是一个 读取 -> 修改 -> 写入。假设现在有两个线程A和B,当前count变量的值为1,A线程对count变量进行了读取,修改的操作,在没有执行写入操作之前发生了系统调度将线程A挂起,线程B开始执行自己的操作,将B线程读取了count变量的值(count = 1),并进行了后续的操作,将计算的count = 2的结果赋值给了count,执行完毕。线程A继续执行,此时同样将count = 2 写入到count中。这样得到最后的结果count就会少记了一次线程的访问。


如何保证线程安全性

    先来看一下线程安全是如何定义的?
    在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。
    Java中提供了锁来保证线程安全性,通过在指定的代码块中添加synchronized或ReentrantLock关键字来保证操作的原子性。
    当线程要访问一个被加锁的对象、方法或者代码块时,会自动获得锁,如果当前的锁被其他线程持有,则会将线程先挂起,等持有锁的线程将锁被释放后会发起信号唤醒阻塞中的线程,去抢占该锁。


synchronized的用法

    synchronized是Java提供的一种内置锁,synchronized的用法可以分为三种
    1. 修饰类
    2. 修饰方法(静态方法|非静态方法)
    3. 修饰代码块(变量)

  1. 修饰类
public class SynchronizedClass{

    public static Object instance = null;

     public Object getInstance() {
         synchronized(SynchronizedClass.class) {
             if (null == instance) {
               return new Instance();
             }
             return instance;
         }
     }
}

当synchronized修饰类时,不管是访问SynchronizedClass类的哪个实例对象,所有线程都会竞争同一把锁。

  1. 修饰静态方法
public class LockStaticMethod{

    public static Object instance = null;

     public synchronized static Object getInstance() {
            if (null == instance) {
               return new Instance();
            }
            return instance;
     }
}

当synchronized修饰静态方法时,不管是访问LockStaticMethod类的哪个实例对象中的getInstance方法,所有线程都会竞争同一把锁。

  1. 修饰非静态方法
public class LockMethod{

    public static Object instance = null;

     public synchronized Object getInstance() {
            if (null == instance) {
               return new Instance();
            }
            return instance;
     }
}

当synchronized修饰非静态方法时,不同实例对象持有的锁是相互不影响的。

  1. 修饰代码块(变量)
public class LockVariable{

    public static Object instance = null;

     public synchronized Object getInstance() {
            synchronized(instance) {
              if (null == instance) {
                 return new Instance();
              }
              return instance;
           }
     }
}

synchronized修饰变量同样也分静态变量和非静态变量,他们的含义同静态方法和非静态方法类似。
下面看一下,当线程访问一个被加锁的方法时,执行过程是怎样的。


image.png

ReentrantLock的用法

    ReentrantLock类是Java 5.0才出现的新的加锁机制,ReentrantLock相比于synchronized提供的加锁机制更加灵活,并且提供了一些synchronized不具备的功能。例如:可中断的锁获取操作、公平队列、非块结构的锁,可定时的锁,可轮训的锁。下面我们来看一下这些特性的使用示例。

  1. 可中断的锁
    可中断的锁提供了对可取消任务加锁的需求,具体使用方式如下所示
    private ReentrantLock lock = new ReentrantLock();

    public void interruptedLock() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            throw new InterruptedException();
        }finally {
            lock.unlock();
        }
    }
  1. 公平队列
    ReentrantLock提供了公平锁,线程按照它们发出请求的顺序来获得锁。ReentrantLock类提供了ReentrantLock(boolean fair)构造函数来初始化公平锁。使用时只要将参数设置为true即可。
ReentrantLock fairLock = new ReentrantLock(true);
  1. 非块结构的锁

  1. 可定时的锁
    ReentrantLock提供了定时的方法tryLock(long timeout, TimeUnit unit),他能根据剩余时间来提供一个时限,如果操作不能在指定的时间内给出结果,那么就会使程序提前结束,示例如下:
public class TimeTryLock {

    public static void main(String[] args) throws Exception {
        ReentrantLock reentrantLock = new ReentrantLock();
        if (reentrantLock.tryLock(100L, TimeUnit.SECONDS)) {
            System.out.println("success");
        }
    }
}
  1. 可轮训的锁
    tryLock方法实现了有条件的获取锁的模式,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。可轮训锁提供了另一种选择:避免发生死锁。
    先来看一下在synchronized模式下死锁的情况
public class SyncDeadLock {

    public void deadLock(Integer v1, Integer v2) {
        synchronized (v1) {
            System.out.println(Thread.currentThread() + "get object lock " + v1);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (v2) {
                System.out.println(Thread.currentThread() + "get object lock " + v2);
            }
        }
    }

    public static void main(String[] args) {
        Integer v1 = 0;
        Integer v2 = 2;
        SyncDeadLock syncDeadLock = new SyncDeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncDeadLock.deadLock(v1, v2);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncDeadLock.deadLock(v2, v1);
            }
        }).start();

    }
}

有了tryLock之后,通过tryLock就可以有效的避免死锁,tryLock可以尝试获取锁,如果没有获取到锁,可以主动将当前持有的锁释放掉,这样可以避免死锁发生的条件,看下面的例子:

public class ReentrantLockDeadLock {

    private static Integer count = 10;

    public boolean tryLock(ReentrantLock lock1, ReentrantLock lock2) {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    System.out.println(Thread.currentThread() + " get " + lock1);
                    if (lock2.tryLock()) {
                        try {
                            System.out.println(Thread.currentThread() + " get " + lock2);
                        } finally {
                            System.out.println(Thread.currentThread() + " unlock " + lock2);
                            lock2.unlock();
                            System.out.println("count = " + --count);
                            if (count < 0) {
                                return true;
                            }
                        }
                    }
                } finally {
                    System.out.println(Thread.currentThread() + " unlock " + lock1);
                    lock1.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLock lock1 = new ReentrantLock();
        ReentrantLock lock2 = new ReentrantLock();
        ReentrantLockDeadLock mainLock = new ReentrantLockDeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.MICROSECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mainLock.tryLock(lock1, lock2);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                mainLock.tryLock(lock2, lock1);
            }
        }).start();

    }
}

你可能感兴趣的:(Java基础-锁)