多线程学习(五)---java锁机制

参考文章:

Java锁机制了解一下
Java不可重入锁和可重入锁理解
重入锁的理解

一、synchronized锁

1.1 synchronized锁是什么

synchronized是Java的一个关键字,它能够将代码块(方法)锁起来。

它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能。

    public synchronized void deal() {
        if (time > 0) {
            System.out.print(Thread.currentThread().getName() + " time is : " + time + " ");
            System.out.println(Thread.currentThread().getName() + "操作:" + --time);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块。

synchronized是一种内置锁/监视器锁,Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的。

1.2 synchronized用处是什么

  1. synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
  2. synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3 synchronized的原理

public class Main {
    //修饰方法
    public synchronized void test1(){

    }

    
    public void test2(){
        // 修饰代码块
        synchronized (this){

        }
    }
}

来反编译看一下:
https://image-static.segmentfault.com/189/693/189693103-5adf16741f7a3
同步代码块: monitorenter和monitorexit指令实现的

同步方法(在这看不出来需要看JVM底层实现):方法修饰符上的ACC_SYNCHRONIZED实现

synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。

具体可参考:

1.4 synchronized如何使用

synchronized一般我们用来修饰三种东西:

  1. 修饰普通方法
  2. 修饰代码块
  3. 修饰静态方法

1.4.1 修饰普通方法

用的锁是Tes t对象(内置锁)

public class Test {
    public synchronized void test() {
        // doSomething
    }
}

1.4.2 修饰代码块

用的锁是Test 对象(内置锁)—>this

public class Test {
    public void test() {
        synchronized (this) {
            // doSomething
        }
    }
}

使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)

public class Test {
    // 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
    private Object object;
    public void test() {
        // 修饰代码块,此时用的锁是自己创建的锁Object
        synchronized (object) {
            // doSomething
        }
    }
}

上面那种方式(随便使用一个对象作为锁)在书上称之为–>客户端锁,这是不建议使用的。

书上想要实现的功能是:给ArrayList添加一个putIfAbsent(),这需要是线程安全的。

方法一:

public class Test<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if (absent) {
            list.add(x);
        }
        return absent;
    }
}

直接添加synchronized是不可行的,因为list.contains(x)用的锁不是Test内置锁。

方法二:

public class Test<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public boolean putIfAbsent(E x) {
        synchronized (list) {
            boolean absent = !list.contains(x);
            if (absent) {
                list.add(x);
            }
            return absent;
        }
    }
}

使用客户端锁,会将当前的实现与原本的list耦合了

方法三:使用组合的方式(也就是装饰器模式)
主要代码:

public class Test<E> implements List<E> {

    private final List<E> list;

    public Test(List<E> list) {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if (absent) {
            list.add(x);
        }
        return absent;
    }

1.4.3 修饰静态方法

获取到的是类锁(类的字节码文件对象):Test.class

public class Test {
    public synchronized static void test() {
        // doSomething
    }
}

等效于:

public class Test {
    public static void test() {
        synchronized (Test.class) {
            // doSomething
        }
    }
}

1.4.4 类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的。

public class Test {

    //synchronized修饰非静态方法
    public synchronized void function() throws InterruptedException {
        for (int i = 0; i <3; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " :function running...");
        }
    }
    //synchronized修饰静态方法
    public static synchronized void staticFunction()
            throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " :Static function running...");
        }
    }

    public static void main(String[] args) {
        final Test demo = new Test();

        // 创建线程执行静态方法
        Thread t1 = new Thread(() -> {
            try {
                staticFunction();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程1");

        // 创建线程执行实例方法
        Thread t2 = new Thread(() -> {
            try {
                demo.function();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程2");
        // 启动
        t1.start();
        t2.start();
    }
}

运行结果:

线程2 :function running...
线程1 :Static function running...
线程2 :function running...
线程1 :Static function running...
线程1 :Static function running...
线程2 :function running...

结果证明:类锁和对象锁是不会冲突的

1.5 重入锁

重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。

线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取。

线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。

重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

1.5.1 不可重入锁

所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。我们尝试设计一个不可重入锁:

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

使用该锁:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。

1.5.2 可重入锁

所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。

同个线程可以进入之前获得锁的同步代码块,这是可重入锁的核心思想

1.5.3 测试

看代码:

public class Widget {
    // 锁住了
    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + " Wigget doSomething ...");
    }
}

public class LoggingWidget extends Widget {
    
    // 锁住了
    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + " LoggingWidget: calling doSomething");
        super.doSomething();
    }
}
  1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁。
  2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰。
  3. 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?
public class Test {

    public static void main(String[] args) {
        LoggingWidget t = new LoggingWidget();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                t.doSomething();
            }
        });
        thread.start();
    }
}

结果:

Thread-0 LoggingWidget: calling doSomething
Thread-0 Wigget doSomething ...

答案是,不需要。

因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!

这就是内置锁的可重入性。

1.6 释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

不会由于异常导致出现死锁现象

二、 Lock显式锁

2.1 Lock显式锁简单介绍

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的。

Lock显式锁是一个接口,我们来看看:
Lock
看注释:

/**
 * {@code Lock} implementations provide more extensive locking
 * operations than can be obtained using {@code synchronized} methods
 * and statements.  They allow more flexible structuring, may have
 * quite different properties, and may support multiple associated
 * {@link Condition} objects.
 *

实现类提供了额外的锁机制,有更好的伸缩性,支持Condition条件对象

 * <p>A lock is a tool for controlling access to a shared resource by
 * multiple threads. Commonly, a lock provides exclusive access to a
 * shared resource: only one thread at a time can acquire the lock and
 * all access to the shared resource requires that the lock be
 * acquired first. However, some locks may allow concurrent access to
 * a shared resource, such as the read lock of a {@link ReadWriteLock}.
 *

通常限定每次一个线程访问共享变量,但ReadWriteLock允许读锁并发访问共享对象

 * <p>The use of {@code synchronized} methods or statements provides
 * access to the implicit monitor lock associated with every object, but
 * forces all lock acquisition and release to occur in a block-structured way:
 * when multiple locks are acquired they must be released in the opposite
 * order, and all locks must be released in the same lexical scope in which
 * they were acquired.
 *

synchronized释放锁的顺序必须是获取锁的相反顺序

 * <p>While the scoping mechanism for {@code synchronized} methods
 * and statements makes it much easier to program with monitor locks,
 * and helps avoid many common programming errors involving locks,
 * there are occasions where you need to work with locks in a more
 * flexible way. For example, some algorithms for traversing
 * concurrently accessed data structures require the use of
 * &quot;hand-over-hand&quot; or &quot;chain locking&quot;: you
 * acquire the lock of node A, then node B, then release A and acquire
 * C, then release B and acquire D and so on.  Implementations of the
 * {@code Lock} interface enable the use of such techniques by
 * allowing a lock to be acquired and released in different scopes,
 * and allowing multiple locks to be acquired and released in any
 * order.
 *

一般来说,使用synchronized来加锁会比较方便,减少出错的概率,但Lock锁的灵活性会更高

 * <p>With this increased flexibility comes additional
 * responsibility. The absence of block-structured locking removes the
 * automatic release of locks that occurs with {@code synchronized}
 * methods and statements. In most cases, the following idiom
 * should be used:
 *
 *  <pre> {@code
 * Lock l = ...;
 * l.lock();
 * try {
 *   // access the resource protected by this lock
 * } finally {
 *   l.unlock();
 * }}</pre>
 *
 * When locking and unlocking occur in different scopes, care must be
 * taken to ensure that all code that is executed while the lock is
 * held is protected by try-finally or try-catch to ensure that the
 * lock is released when necessary.
 *

灵活性高,意味着出错几率也会变高,介绍了一般使用Lock锁的方法,记得释放锁

 * <p>{@code Lock} implementations provide additional functionality
 * over the use of {@code synchronized} methods and statements by
 * providing a non-blocking attempt to acquire a lock ({@link
 * #tryLock()}), an attempt to acquire the lock that can be
 * interrupted ({@link #lockInterruptibly}, and an attempt to acquire
 * the lock that can timeout ({@link #tryLock(long, TimeUnit)}).
 *

获取锁是非阻塞、能被中断、可以设置超时的

 * <p>A {@code Lock} class can also provide behavior and semantics
 * that is quite different from that of the implicit monitor lock,
 * such as guaranteed ordering, non-reentrant usage, or deadlock
 * detection. If an implementation provides such specialized semantics
 * then the implementation must document those semantics.
 *

提高语义化,知道哪里加锁了,哪里释放锁

 * <p>Note that {@code Lock} instances are just normal objects and can
 * themselves be used as the target in a {@code synchronized} statement.
 * Acquiring the
 * monitor lock of a {@code Lock} instance has no specified relationship
 * with invoking any of the {@link #lock} methods of that instance.
 * It is recommended that to avoid confusion you never use {@code Lock}
 * instances in this way, except within their own implementation.
 *

建议在使用的时候不要使用Lock作为内置锁,因为会导致混乱

 * <p>Except where noted, passing a {@code null} value for any
 * parameter will result in a {@link NullPointerException} being
 * thrown.
 *
 * <h3>Memory Synchronization</h3>
 *
 * <p>All {@code Lock} implementations <em>must</em> enforce the same
 * memory synchronization semantics as provided by the built-in monitor
 * lock, as described in
 * <a href="https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4">
 * The Java Language Specification (17.4 Memory Model)</a>:
 * <ul>
 * <li>A successful {@code lock} operation has the same memory
 * synchronization effects as a successful <em>Lock</em> action.
 * <li>A successful {@code unlock} operation has the same
 * memory synchronization effects as a successful <em>Unlock</em> action.
 * </ul>
 *
 * Unsuccessful locking and unlocking operations, and reentrant
 * locking/unlocking operations, do not require any memory
 * synchronization effects.
 *

实现内存可见性

 * <h3>Implementation Considerations</h3>
 *
 * <p>The three forms of lock acquisition (interruptible,
 * non-interruptible, and timed) may differ in their performance
 * characteristics, ordering guarantees, or other implementation
 * qualities.  Further, the ability to interrupt the <em>ongoing</em>
 * acquisition of a lock may not be available in a given {@code Lock}
 * class.  Consequently, an implementation is not required to define
 * exactly the same guarantees or semantics for all three forms of
 * lock acquisition, nor is it required to support interruption of an
 * ongoing lock acquisition.  An implementation is required to clearly
 * document the semantics and guarantees provided by each of the
 * locking methods. It must also obey the interruption semantics as
 * defined in this interface, to the extent that interruption of lock
 * acquisition is supported: which is either totally, or only on
 * method entry.
 *
 * <p>As interruption generally implies cancellation, and checks for
 * interruption are often infrequent, an implementation can favor responding
 * to an interrupt over normal method return. This is true even if it can be
 * shown that the interrupt occurred after another action may have unblocked
 * the thread. An implementation should document this behavior.
 *

具体的实现类相关

 * @see ReentrantLock
 * @see Condition
 * @see ReadWriteLock
 *
 * @since 1.5
 * @author Doug Lea
 */

可以简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的
  • 提高了语义化,哪里加锁,哪里解锁都得写出来
  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
  • 支持Condition条件对象
  • 允许多个读线程同时访问共享资源

2.2 synchronized锁和Lock锁使用哪个

前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??

必须是有的。Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化。

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大,而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)

所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁

2.3 公平锁

公平锁:

  • 线程将按照它们发出请求的顺序来获取锁

非公平锁:

  • 线程发出请求的时可以“插队”获取锁

Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

公平锁会来带一些性能的消耗

三、总结

synchronized好用,简单,性能不差

没有使用到Lock显式锁的特性就不要使用Lock锁了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值