`

浅谈Java中的锁:Synchronized、重入锁、读写锁

阅读更多

Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制


Synchronized

首先我们来看一段简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NotSyncDemo {
    public static int i=0;
    static class ThreadDemo extends Thread {
        @Override
        public void run() {
           for (int j=0;j<10000;j++){
               i++;
           }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1=new ThreadDemo();
        ThreadDemo t2=new ThreadDemo();
        t1.start();t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上方的代码使用了2个线程同时对静态变量i进行++操作,理想中的结果最后输出的i的值应该是20000才对,但是如果你执行这段代码的时候你会发现最后的结果始终是一个比20000小的数。这个就是由于JMM规定线程操作变量的时候只能先从主内存读取到工作内存,操作完毕后在写到主内存。而当多个线程并发操作一个变量时很可能就会有一个线程读取到另外一个线程还没有写到主内存的值从而引起上方的现象。更多关于JMM的知识请参考此文章:Java多线程内存模型

想要避免这种多线程并发操作引起的数据异常问题一个简单的解决方案就是加锁。JDK提供的synchronize就是一个很好的选择。
synchronize的作用就是实现线程间的同步,使用它加锁的代码同一时刻只能有一个线程访问,既然是单线程访问那么就肯定不存在并发操作了。
synchronize可以有多种用法,下面给出各个用法的示例代码。


Synchronized的三种使用方式

给指定对象加锁,进入代码前需要获得对象的锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SyncObjDemo {
    public static Object obj = new Object();
    public static int i = 0;
    static class ThreadDemo extends Thread {
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                synchronized (obj) {
                    i++;
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo();
        ThreadDemo t2 = new ThreadDemo();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

给方法加锁,相当于给当前实例加锁,进入代码前需要获得当前实例的锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SyncMethodDemo {
    public static int i = 0;
    static class ThreadDemo extends Thread {
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                 add();
            }
        }
        public synchronized void add(){
            i++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo threadDemo=new ThreadDemo();
        Thread t1 = new Thread(threadDemo);
        Thread t2 = new Thread(threadDemo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

给静态方法加锁,相当于给当前类加锁,进入代码前需要获得当前类的锁。这种方式请慎用,都锁住整个类了,那效率能高哪去

1
2
3
public static synchronized void add(){
            i++;
        }


重入锁

在JDK6还没有优化synchronize之前还有一个锁比它表现的更为亮眼,这个锁就是重入锁。
我们来看一下一个简单的使用重入锁的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ReentrantLockDemo {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    static class ThreadDemo extends Thread {
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                lock.lock();
                 try {
                     i++;
                 }finally {
                     lock.unlock();
                 }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo();
        ThreadDemo t2 = new ThreadDemo();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上方代码使用重入锁同样实现了synchronize的功能。并且呢,我们可以看到使用冲入锁是显示的指定什么时候加锁什么时候释放的,这样对于一些流程控制就会更加的有优势。

再来看这个锁为什么叫做重入锁呢,这是因为这种锁是可以反复进入的,比如说如下操作是允许的。

1
2
3
4
5
6
7
8
lock.lock();
lock.lock();
try {
  i++;
}finally {
    lock.unlock();
    lock.unlock();
}

不过需要注意的是如果多次加锁的话同样也要记得多次释放,否则资源是不能被其他线程使用的。

在之前的文章:多线程基本概念 中有提到过因为线程优先级而导致的饥饿问题,重入锁提供了一种公平锁的功能,可以忽略线程的优先级,让所有线程公平竞争。使用公平锁的方式只需要在重入锁的构造方法传入一个true就可以了。

1
public static ReentrantLock lock = new ReentrantLock(true);

重入锁还提供了一些高级功能,例如中断。
对于synchronize来说,如果一个线程获取资源的时候要么阻塞要么就是获取到资源,这样的情况是无法解决死锁问题的。而重入锁则可以响应中断,通过放弃资源而解决死锁问题。
使用中断的时候只需要把原先的lock.lock()改成lock.lockInterruptibly()就OK了。
来看代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ReentrantLockInterruptDemo {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    static class ThreadDemo extends Thread {
        int i = 0;
        public ThreadDemo(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                if (i == 1) {
                    lock1.lockInterruptibly();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock1.lockInterruptibly();
                }
                System.out.println(Thread.currentThread().getName() + "完成任务");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "退出");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ThreadDemo(1),"t1");
        Thread t2 = new Thread(new ThreadDemo(2),"t2");
        t1.start();
        t2.start();
        Thread.sleep(1500);
        t1.interrupt();
    }
}

查看上方代码我们可以看到,线程t1启动后先占有lock1,然后会在睡眠1秒之后试图占有lock2,而t2则先占有lock2,然后试图占有lock1。这个过程则势必会发生死锁。而如果再这个时候我们给t1一个中断的信号t1就会响应中断从而放弃资源,继而解决死锁问题。

除了提供中断解决死锁以外,重入锁还提供了限时等待功能来解决这个问题。
限时等待的使用方式是使用lock.tryLock(2,TimeUnit.SECONDS)
这个方法有两个参数,前面是等待时长,后面是等待时长的计时单位,如果在等待时长范围内获取到了锁就会返回true。

请看代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ReentrantLockTimeDemo {
    public static ReentrantLock lock = new ReentrantLock();
    static class ThreadDemo extends Thread {
        @Override
        public void run() {
            try {
                if (lock.tryLock(2, TimeUnit.SECONDS)) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取锁成功");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + "获取锁失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ThreadDemo(), "t1");
        Thread t2 = new Thread(new ThreadDemo(), "t2");
        t1.start();
        t2.start();
    }
}

同样的tryLock也可以不带参数,不带参数的时候就是表示立即获取,获取不成功就直接返回false

我们知道synchronize配合wait和notify可以实现等待通知的功能,重入锁同样也提供了这种功能的实现。那就是condition。使用lock.newCondition()就可以获得一个Condition对象。

下面请看使用Condition的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ReentrantLockWaitNotifyThread {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    static class WaitThreadDemo extends Thread {
        @Override
        public void run() {
            try {
                System.out.println("WaitThread wait,time=" + System.currentTimeMillis());
                lock.lock();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
                System.out.println("WaitThread end,time=" + System.currentTimeMillis());
            }
        }
    }
    static class NotifyThreadDemo extends Thread {
        @Override
        public void run() {
                lock.lock();
                System.out.println("NotifyThread notify,time=" + System.currentTimeMillis());
                condition.signal();
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                    System.out.println("NotifyThread end,time=" + System.currentTimeMillis());
                }
            }
    }

    public static void main(String[] args) {
        WaitThreadDemo waitThreadDemo = new WaitThreadDemo();
        NotifyThreadDemo notifyThreadDemo = new NotifyThreadDemo();
        waitThreadDemo.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyThreadDemo.start();
    }
}


读写锁

通过上方的内容我们知道了为了解决线程安全问题,JDK提供了相当多的锁来帮助我们。但是如果多线程并发读的情况下是不会出现线程安全问题的,那么有没有一种锁可以在读的时候不控制,读写冲突的时候才会控制呢。答案是有的,JDK提供了读写分离锁来实现读写分离的功能。

这里给出使用读写锁的一个代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ReadWriteLockDemo {
    public static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    public static Lock readLock = readWriteLock.readLock();
    public static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        lock.lock();
        try {
            System.out.println("readTime:" + System.currentTimeMillis());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock) {
        lock.lock();
        try {
            System.err.println("writeTime:" + System.currentTimeMillis());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    static class ReadThread extends Thread {
        @Override
        public void run() {
            read(readLock);
        }
    }

    static class WriteThread extends Thread {
        @Override
        public void run() {
            write(writeLock);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new ReadThread().start();
        }
        new WriteThread().start();
        new WriteThread().start();
        new WriteThread().start();
    }
}

上方代码模拟了10个线程并发读,3个线程并发写的状况,如果我们使用synchronize或者重入锁的时候我想上方最后的耗时应该是26秒多。但是如果你执行 一下上方的代码你就会发现仅仅只花费了6秒多。这就是读写锁的魅力。

本文所有源码https://github.com/shiyujun/syj-study-demo


 
 

  • 大小: 18.4 KB
  • 大小: 171.6 KB
分享到:
评论

相关推荐

    java锁机制Synchronizedjava锁机制Synchronized

    Java 锁机制 Synchronized 是 Java 语言中的一种同步机制,用于解决多线程并发访问共享资源时可能出现的一些问题。 Java 锁机制 Synchronized 的概念 在 Java 中,每个对象都可以被看作是一个大房子,其中有多个...

    Java并发编程:Synchronized关键字深度解析

    本文深入探讨了Java中用于解决并发编程中线程安全问题的synchronized关键字。文章首先讨论了多线程编程中临界资源的概念,包括对象、变量、文件等,以及同步机制的必要性。重点解析了synchronized的工作原理,包括其...

    java锁详解.pdf

    5. 可重入锁:ReentrantLock 锁支持可重入锁机制,允许线程多次获得锁。 三、Volatile 原理 1. volatile 关键字:volatile 关键字用于声明变量,可以确保变量的可见性和禁止指令重排。 2. volatile 的应用:...

    各种锁汇总,乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁、行级锁等

    本文将深入探讨标题和描述中提及的各种锁,包括乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁以及行级锁。 1. **乐观锁**:乐观锁假设多线程环境中的冲突较少,所以在读取数据时不加锁,只有...

    java 偏向锁、轻量级锁及重量级锁synchronized原理.docx

    Java中的`synchronized`关键字是实现线程安全的关键机制,它基于Java对象头的Mark Word进行锁的状态管理。Mark Word是一个动态变化的数据结构,用于存储对象的HashCode、分代年龄、锁状态标志等信息。在32位JVM中,...

    java中synchronized用法

    "Java 中 synchronized 用法详解" Synchronized 是 Java 语言中用于解决多线程共享数据同步问题的关键字。它可以作为函数的修饰符,也可以作为函数内的语句,用于实现同步方法和同步语句块。在 Java 中,...

    Java 同步锁(synchronized)详解及实例

    Java中的同步锁,即`synchronized`关键字,是Java多线程编程中用于解决并发问题的重要机制。它确保了对共享资源的互斥访问,防止数据的不一致性。当我们有多线程环境并涉及到共享数据时,可能会出现竞态条件,就像...

    彻底理解Java中的各种锁.pdf

    本文将详细介绍Java中包括乐观锁、悲观锁、自旋锁、可重入锁、读写锁等多种锁机制的概念、特点、应用场景及其优化技术。 1. 乐观锁与悲观锁 乐观锁与悲观锁反映了对数据并发访问策略的不同预期。乐观锁假设数据通常...

    java锁机制Synchronized.pdf

    java锁机制Synchronized.pdf

    关于读写锁算法的Java实现及思考

    在Java中,`java.util.concurrent.locks`包下的`ReadWriteLock`接口提供了读写锁的抽象定义,具体实现由`ReentrantReadWriteLock`类提供。`ReentrantReadWriteLock`实现了`ReadWriteLock`接口,提供了`readLock()`和...

    java同步synchronized关键字用法示例

    在Java 5之后,引入了`java.util.concurrent`包,其中的`ReentrantLock`类提供了可重入锁,它具有与`synchronized`相似的功能,但更加灵活,支持公平锁、非公平锁以及可中断的锁等待。 **5. synchronized的应用示例...

    java代码-证明synchronized可重入锁

    在Java中,由于`synchronized`的可重入性,这样的情况不会发生。 此外,可重入锁还有助于避免递归调用中的死锁问题。例如,一个线程在执行递归函数时,每次进入函数都需要获得锁,如果锁不可重入,那么递归调用将...

    自己动手写一把可重入锁测试案例

    在Java中,java.util.concurrent.locks.ReentrantLock类便是可重入锁的实现。 首先,理解可重入锁的工作原理至关重要。可重入锁通过记录持有锁的线程以及获取锁的次数来实现其功能。当线程尝试获取锁时,如果当前锁...

    java并发锁面试知识

    java中的乐观锁与悲观锁,synchronized与ReentrantLock重入锁的说明与比较

    java里面synchronized用法.doc

    Java 中的 synchronized 用法详解 Java 中的 synchronized 关键字是用于解决多线程并发问题的重要工具之一。它可以被用于方法、代码块和变量上,以实现对共享资源的互斥访问控制。本文将对 Java 中的 synchronized ...

    Java synchronized使用案例

    Java中的`synchronized`关键字是多线程编程中的一个重要概念,用于控制并发访问共享资源,以保证数据的一致性和完整性。这个关键词提供了互斥锁机制,防止多个线程同时执行同一段代码,确保了线程安全。 一、`...

    Java并发编程:Synchronized及其实现原理

    在某些场景下,可以考虑使用其他并发控制机制,如ReentrantLock可重入锁、Semaphore信号量、ReadWriteLock读写锁等。 总结,Synchronized是Java并发编程的重要组成部分,它通过锁机制保证了线程安全,但在实际应用...

    java 多线程synchronized互斥锁demo

    总结来说,Java中的`synchronized`关键字是实现线程同步的关键,它通过互斥锁确保对共享资源的访问是线程安全的。在多线程编程中,合理使用`synchronized`可以有效避免竞态条件,保证程序的正确性和稳定性。对于...

Global site tag (gtag.js) - Google Analytics