美文网首页Java基础
并发编程(二)初识synchronized与ReentrantL

并发编程(二)初识synchronized与ReentrantL

作者: Timmy_zzh | 来源:发表于2020-12-24 23:14 被阅读0次
  • Synchronized
    • 修饰方法,代码块
    • 类锁,对象锁
  • synchronized实现原理:monitorenter与monitorexit指令
  • ReentrantLock
    • 需要开发人员手动释放锁
  • 公平锁
  • 读写锁

1.Synchronized

synchronized可以用来修饰以下3个层面
  • 修饰实例方法
  • 修饰静态类方法
  • 修饰代码块
1.1.Synchronized修饰实例方法 -- 对象锁
public class _02SynchronizedMethod {

    public static void main(String[] args) {
        _02SynchronizedMethod demo1 = new _02SynchronizedMethod();
        _02SynchronizedMethod demo2 = new _02SynchronizedMethod();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo2.printLog();
            }
        });

        thread1.start();
        thread2.start();
    }

    private synchronized void printLog() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread Name:" + Thread.currentThread().getName() + " --- i:" + i);
                Thread.sleep(300);
            }
        } catch (Exception e) {
        }
    }
}

日志输出:
> Task :testlib:_02SynchronizedMethod.main()
Thread Name:Thread-1 --- i:0
Thread Name:Thread-0 --- i:0
Thread Name:Thread-1 --- i:1
Thread Name:Thread-0 --- i:1
Thread Name:Thread-1 --- i:2
Thread Name:Thread-0 --- i:2
Thread Name:Thread-1 --- i:3
Thread Name:Thread-0 --- i:3
Thread Name:Thread-1 --- i:4
Thread Name:Thread-0 --- i:4
  • 可以看出,两个线程是交互执行的,上诉情况下的锁对象是当前实例对象,不同对象实例之间方法调用不会有互斥效果。

代码修改为不同线程调用同一个对象实例的方法

public class _02SynchronizedMethod {

    public static void main(String[] args) {
        _02SynchronizedMethod demo1 = new _02SynchronizedMethod();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });

        thread1.start();
        thread2.start();
    }

    private synchronized void printLog() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread Name:" + Thread.currentThread().getName() + " --- i:" + i);
                Thread.sleep(300);
            }
        } catch (Exception e) {
        }
    }
}

日志输出:
> Task :testlib:_02SynchronizedMethod.main()
Thread Name:Thread-0 --- i:0
Thread Name:Thread-0 --- i:1
Thread Name:Thread-0 --- i:2
Thread Name:Thread-0 --- i:3
Thread Name:Thread-0 --- i:4
Thread Name:Thread-1 --- i:0
Thread Name:Thread-1 --- i:1
Thread Name:Thread-1 --- i:2
Thread Name:Thread-1 --- i:3
Thread Name:Thread-1 --- i:4
  • 可以看出:只有某一个线程中的代码执行完之后,才会调用另一个线程中的代码。说明两个线程是互斥的。
1.2.修饰静态类方法

使用synchronized修饰的静态方法,锁对象使用的是当前类的Class对象。因此即使在不同线程中调用不同实例对象,也会有互斥效果。

package com.timmy.testlib;

public class _03SynchronizedStaticMethod {

    public static void main(String[] args) {
        _03SynchronizedStaticMethod demo1 = new _03SynchronizedStaticMethod();
        _03SynchronizedStaticMethod demo2 = new _03SynchronizedStaticMethod();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo2.printLog();
            }
        });

        thread1.start();
        thread2.start();
    }

    private static synchronized void printLog() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread Name:" + Thread.currentThread().getName() + " --- i:" + i);
                Thread.sleep(300);
            }
        } catch (Exception e) {
        }
    }
}

日志输出:
> Task :testlib:_03SynchronizedStaticMethod.main()
Thread Name:Thread-0 --- i:0
Thread Name:Thread-0 --- i:1
Thread Name:Thread-0 --- i:2
Thread Name:Thread-0 --- i:3
Thread Name:Thread-0 --- i:4
Thread Name:Thread-1 --- i:0
Thread Name:Thread-1 --- i:1
Thread Name:Thread-1 --- i:2
Thread Name:Thread-1 --- i:3
Thread Name:Thread-1 --- i:4
  • 两个线程是依次执行的
1.3.synchronized修饰代码块
package com.timmy.testlib;

public class _02_3Synchronized {

    private Object lock = new Object();

    public static void main(String[] args) {
        _02_3Synchronized demo1 = new _02_3Synchronized();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });

        thread1.start();
        thread2.start();
    }

    private void printLog() {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread Name:" + Thread.currentThread().getName() + " --- i:" + i);
            }
        }
    }
}

日志输出:
> Task :testlib:_02_3Synchronized.main()
Thread Name:Thread-0 --- i:0
Thread Name:Thread-0 --- i:1
Thread Name:Thread-0 --- i:2
Thread Name:Thread-0 --- i:3
Thread Name:Thread-0 --- i:4
Thread Name:Thread-1 --- i:0
Thread Name:Thread-1 --- i:1
Thread Name:Thread-1 --- i:2
Thread Name:Thread-1 --- i:3
Thread Name:Thread-1 --- i:4
  • synchronized作用域代码块时,锁对象就是后面括号中的对象,Object对象都可以当作锁对象
1.4.synchronized实现原理:monitorenter 与 monitorexit 字节码指令
如下代码synchronized修饰代码块:
    public int add(int i) {
        synchronized (this) {
            int j = 10;
            num = num + i;
            return num;
        }
    }

javap 查看对应字节码:
  public int add(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: bipush        10
         6: istore_3
         7: aload_0
         8: aload_0
         9: getfield      #2                  // Field num:I
        12: iload_1
        13: iadd
        14: putfield      #2                  // Field num:I
        17: aload_0
        18: getfield      #2                  // Field num:I
        21: aload_2
        22: monitorexit
        23: ireturn
        24: astore        4
        26: aload_2
        27: monitorexit
        28: aload         4
        30: athrow
  • 如上add方法字节码中有1个monitorenter和2个monitorexit指令。这是因为虚拟机需要保证当异常发生时也能释放锁。
    • 因此2个monitorexit一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。
synchronized关键字修饰方法字节码实现:
    public synchronized int add(int i) {
        num = num + i;
        return num;
    }
对应字节码:
  public synchronized int add(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: aload_0
         2: getfield      #2                  // Field num:I
         5: iload_1
         6: iadd
         7: putfield      #2                  // Field num:I
        10: aload_0
        11: getfield      #2                  // Field num:I
        14: ireturn
  • 上述代码可以看出,被synchronized修饰的方法在被编译为字节码后,在方法的flags属性中会被标记为ACC_SYNCHRONIZED标志。
    • 当虚拟机访问一个被标记为ACC_SYNCHRONIZED的方法时,会自动在方法的开始和结束(异常)位置添加monitorenter和monitorexit指令
关于monitorenter和monitorexit,可以理解为一把具体的锁。
  • 在这个锁中保存着两个比较重要的属性:计数器和指针
    • 计数器代表当前线程一共访问了几次这把锁
    • 指针指向持有这把锁的线程

如图:

1.monitorenter和monitorexit指令原理图png.png
  • 锁计数器默认为0
    • 当执行monitorenter指令时,如锁计数器值为0说明这把锁并没有被其他线程持有,那么这个线程会将计数器加1,并将锁中的指针指向自己。
    • 当执行monitorexit指令时,会将计数器减1.

2.ReentrantLock

2.1.ReentrantLock基本使用
  • ReentrantLock的使用与synchronized不同,它的加锁和解锁都需要手动完成,如下:
public class _03ReentrantLockDemo {
    ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        _03ReentrantLockDemo demo1 = new _03ReentrantLockDemo();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.printLog();
            }
        });

        thread1.start();
        thread2.start();
    }

    private void printLog() {
        try {
            reentrantLock.lock();
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread Name:" + Thread.currentThread().getName() + " --- i:" + i);
                Thread.sleep(300);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }
}

日志打印:
> Task :testlib:_03ReentrantLockDemo.main()
Thread Name:Thread-0 --- i:0
Thread Name:Thread-0 --- i:1
Thread Name:Thread-0 --- i:2
Thread Name:Thread-0 --- i:3
Thread Name:Thread-0 --- i:4
Thread Name:Thread-1 --- i:0
Thread Name:Thread-1 --- i:1
Thread Name:Thread-1 --- i:2
Thread Name:Thread-1 --- i:3
Thread Name:Thread-1 --- i:4
  • 解析
    • 使用ReentrantLock也能实现同synchronized相同的效果
  • ReentrantLock与synchronized不同
    • synchronized当异常发生时,synchronized会自动释放锁,但是ReentrantLock并不会自动释放锁,因此好的方式是将ReentrantLock的unlock操作放在finally代码块中,保证在任何时候锁都能够被正常释放掉
2.2.公平锁实现
  • 公平锁通过ReentrantLock传入的参数为true而实现
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • 默认情况下,synchronized和ReentrantLock都是非公平锁,但是ReentrantLock可以通过传入true来创建一个公平锁
    • 所谓公平锁就是通过同步队列来实现多个线程按照申请锁的顺序获取锁

代码使用

public class _03_2FairReentrantLock implements Runnable {

    private int shareNum = 0;
    private ReentrantLock reentrantLock = new ReentrantLock(true);

    @Override
    public void run() {
        while (shareNum < 10) {
            try {
                reentrantLock.lock();
                shareNum++;
                System.out.println(Thread.currentThread().getName() + " ,shareNum:" + shareNum);
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        _03_2FairReentrantLock reentrantLock = new _03_2FairReentrantLock();
        Thread thread1 = new Thread(reentrantLock);
        Thread thread2 = new Thread(reentrantLock);
        Thread thread3 = new Thread(reentrantLock);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

日志输出:
> Task :testlib:_03_2FairReentrantLock.main()
Thread-0 ,shareNum:1
Thread-1 ,shareNum:2
Thread-2 ,shareNum:3
Thread-0 ,shareNum:4
Thread-1 ,shareNum:5
Thread-2 ,shareNum:6
Thread-0 ,shareNum:7
Thread-1 ,shareNum:8
Thread-2 ,shareNum:9
Thread-0 ,shareNum:10
Thread-1 ,shareNum:11
Thread-2 ,shareNum:12 
--可以看出创建的3个线程依次按照顺序去修改shareNum的值
2.3.读写锁
读写锁的由来
  • 在日常开发中,经常会定义一个线程间共享的用作缓存的数据结构,比如一个较大的Map,缓存着全部的城市id和城市name对应关系,
    • 这个Map绝大部分时间提供读服务。而写操作占有的时间很少,通常是在服务启动时初始化,然后可以每隔一定时间再刷新缓存的数据。
    • 但是写操作开始到结束之间,不能再有其他读操作进来,并且写操作完成之后的更新数据需要对后续的读操作可见。
  • 在没有读写锁支持的时候,如果想要完成上述工作就需要使用java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行。这样做的目的是使读操作能读取到正确的数据,不会出现脏读。
  • 但是使用concurrent包中的读写锁(ReentrantReadWriteLock)实现上述功能,就只需要在读操作时获取读锁,写操作时获取写锁即可。
    • 当写锁被获取时,后续的读写锁都会被阻塞,写锁释放之后,所有操作继续执行,这种编程方式相对于等待通知机制的实现而言,变得简单明了。

读写锁的使用:

创建读写锁:
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
获取读写锁:
readWriteLock.readLock()
readWriteLock.writeLock()

使用读写锁读写数据:

public class _03_3ReadWriteLock {

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    private static String value = "0";

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Reader(), "read lock -1 ");
        Thread thread2 = new Thread(new Reader(), "read lock -2 ");
        Thread thread3 = new Thread(new Writer(), "write lock");
        thread1.start();
        thread2.start();
        thread3.start();
    }

    static class Reader implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    readWriteLock.readLock().lock();
                    System.out.println(Thread.currentThread().getName() + " ,i:" + i + " , value:" + value);
                } finally {
                    readWriteLock.readLock().unlock();
                }
            }
        }
    }

    static class Writer implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i < 7; i += 2) {
                try {
                    readWriteLock.writeLock().lock();
                    System.out.println(Thread.currentThread().getName() + " ,i:" + i);
                    value = value.concat(String.valueOf(i));
                } finally {
                    readWriteLock.writeLock().unlock();
                }
            }
        }
    }
}

日志输出:
> Task :testlib:_03_3ReadWriteLock.main()
read lock -1  ,i:0 , value:0
read lock -2  ,i:0 , value:0
write lock ,i:1
read lock -1  ,i:1 , value:01
read lock -2  ,i:1 , value:01
write lock ,i:3
read lock -1  ,i:2 , value:013
read lock -2  ,i:2 , value:013
write lock ,i:5
read lock -1  ,i:3 , value:0135
read lock -2  ,i:3 , value:0135
read lock -1  ,i:4 , value:0135
read lock -2  ,i:4 , value:0135
read lock -2  ,i:5 , value:0135
read lock -2  ,i:6 , value:0135
read lock -1  ,i:5 , value:0135
read lock -1  ,i:6 , value:0135
read lock -2  ,i:7 , value:0135
read lock -2  ,i:8 , value:0135
read lock -2  ,i:9 , value:0135
read lock -1  ,i:7 , value:0135
read lock -1  ,i:8 , value:0135
read lock -1  ,i:9 , value:0135
  • 解析:通过查看日志,可以看出当写入操作在执行时,读取数据的操作会被阻塞。当写入操作执行成功后,读取数据的操作继续执行,并且读取的数据也是最新写入后的实时数据

相关文章

网友评论

    本文标题:并发编程(二)初识synchronized与ReentrantL

    本文链接:https://www.haomeiwen.com/subject/edivnktx.html