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 { 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 { 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 { 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; } 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秒多。这就是读写锁的魅力。
相关推荐
java锁机制Synchronized java锁机制Synchronized java锁机制Synchronized java锁机制Synchronized
java锁机制Synchronized.pdf
主要介绍了Java 同步锁(synchronized)详解及实例的相关资料,需要的朋友可以参考下
java中synchronized用法
java中的乐观锁与悲观锁,synchronized与ReentrantLock重入锁的说明与比较
java锁机制Synchronized[归纳].pdf
java代码-证明synchronized可重入锁
java中synchronized的使用,java中的锁锁的到底是什么?是括号里的代码块吗?肯定不是的;
java锁机制Synchronized参考.pdf
并发编程原理学习:synchronized关键字.doc
lock锁,lock锁和synchronized的对比 # Lock锁 JDK5.0后Java提供了一种更加强大的线程同步机制。一种显式定义同步锁对象来实现锁,提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问...
Synchronized是Java中解决并发问题的一种常用的方法,也是简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。从语法...
乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。 悲观锁:总是假设最坏的情况,...
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32...
Java中的synchronized:同步方法与线程安全
在Java中,synchronized关键字是用来控制线程同步的,是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。Synchronized既可以对代码块使用,也可以加在整个方法上。 关键是,不要认为给方法或者...