Java互斥访问机制
- 实现java多线程中共享的、可修改的数据,必然涉及到并发访问问题,因此需要通过互斥同步机制来实现
- 实现方法,synchronized,锁机制
synchronized
- 非公平,悲观,独享,互斥,可重入重量级锁
- JVM 层面实现,不但可以通过监控工具监控synchronized的锁定,而且在代码执行异常时,jvm会自动释放锁定,但是Lock不需(需要通过finally的unLock方法释放)
- 可以添加在代码块,方法(实际就是将方法语句全部用synchronized包裹起来)
Lock
- 有多种类锁,按分类有自旋锁,偏向锁,阻塞锁,可重入锁,读写时,互斥锁,乐观锁,悲观锁,公平锁,偏向锁,对象锁
- 锁存在降级(在进入safepoint时会判断是否有限制monitor,然后试图降级),消除,粗化
- ReetrantLock,默认非公平但可实现公平(实例传参为true即可实现公平),悲观,独享,互斥,可重入,重量级锁
- ReentranLock,默认非公平但可实现公平(实例传参为true即可实现公平),悲观,写独享,读共享,可重入,重量级锁
- ReentranLock实现方式
- 1.Lock(),获取锁立即返回。如果其它线程持有锁,当前线程一致处于休眠状态,直到获取锁。
- 2.tryLock(),如果获取了锁立即返回true。如果其它线程持有锁,立即返回false
- 3.tryLock(long timeout,TimeUnit unit),如果获取锁定立即返回true;如果别的线程正持有锁,会等待参数给的的时间,等待过程获取锁返回true;等待超时返回false
ReetranLock与Synchornized
-
实现层面
- synchronized,jvm层面实现,可以使用监控工具去查看监控synchronized的锁定,而且保证代码执行出现异常时,jvm会自动释放锁定,但Lock不需
- ReetranLock,代码层面实现,无法通过监控工具去查看锁定情况,锁的实现必须将unLock()放置finally{}中同步、阻塞式API,简单实现,要点如下
-
公平性
- synchronized,非公平性,悲观,独享,互斥,可重入重量级锁
- ReentrantLock,默认非公平,实例化时传入参数true即可实现公平锁
-
中断等待
如果A和B都要获得对象O的锁定,假设A获取对象O锁,B将等待A释放对O的锁定 1.synchronized: 如果A不释放,B将一致等待下去,不能被中断 2.ReentrantLock:拥有synchronized相同语义,还多了锁投票,定时锁等候,中断锁等候。 如果A不释放,可以使B在等待了足够长时间以后,中断等待,转而去干别的事情。 因此在高竞态的并发环境下,并发吞吐量会高于synchronized
-
更精细的同步操作
1.相比synchronized通过wait(),notify(),sleep()等方法只能指定一个隐含条件去进行同步操作,多一个条件则需要再加一个锁 2.ReentranLock实现精细同步操作: a.带超时获取锁尝试 b.可以判断是否有线程,或某个特定线程在排队时等待获取锁 c.可以响应中断请求 3.ReentranLock通过条件变量(Condition)对象同时绑定多个Condition对象,实现一把锁绑定多个条件 a.将wait,notify,notifyAll,signal等转化为响应对象, b.将复杂晦涩同步操作转变为直观可控对象行为 c.可以进行多个条件绑定实现同步操作。
-
性能
1.早期synchronized标记低效,相对ReentrantLock,大部分场景都比较差 2.java6 对synchronized有非常大改进,在高竞态情况下,ReentranLock仍有一定优势(中断等待,转而去干别的事情)
知识扩展
线程安全的意义
- 线程安全,在多线程环境下共享的、可修改的数据(对象,变量)的正确性
- 多线程环境下,如果未对共享、可修改数据进行互斥访问,则会出现数据脏读、不一致等现象
- 线程安全,保证多线程环境,多个线程访问共享、可修改数据时,不会出现脏读,不一致线性
- 保证线程安全的方法
1.封装:通过封装,我们可以将对象内部状态隐藏、保护起来 2.不可变:将数据对象设置为不可变。java目前无真正意义的原生不可变。
线程安全的基本特性
原子性
-
定义:相关操作不会中途被其他线程干扰,一般通过同步机制实现
-
非原子性操作
例子:通过取两次数值然后进行对比,来模拟两次对共享状态(变量)的操作。 1.执行后可以看到仅仅两个线程低度并发,就非常容易碰到former,latter变量不相等情况。 2.原因是在取值过程中,其他线程可能已经对共享状态进行修改 3.代码及运行结果如下 public class ThreadSafeSample { public int sharedState; public void nonSafeAction() { while (sharedState < 100000) { int former = sharedState++; int latter = sharedState; if (former != latter - 1) { System.out.printf("Observed data race, former is " + former + ", " + "latter is " + latter); } } } public static void main(String[] args) throws InterruptedException { ThreadSafeSample sample = new ThreadSafeSample(); Thread threadA = new Thread(){ public void run(){ sample.nonSafeAction(); } }; Thread threadB = new Thread(){ public void run(){ sample.nonSafeAction(); } }; threadA.start(); threadB.start(); threadA.join(); threadB.join(); } } 运行结果: java ThreadSafeSample Observed data race, former is 13097, latter is 13099
-
添加synchronized,锁定最小的代码段,对类的对象级别进行互斥访问,实现原子操作
1.代码 public class ThreadSafeSample { public int sharedState; public void nonSafeAction() { while (sharedState < 100000) { sychronized(this){ int former = sharedState++; int latter = sharedState; } if (former != latter - 1) { System.out.printf("Observed data race, former is " + former + ", " + "latter is " + latter); } } } public static void main(String[] args) throws InterruptedException { ThreadSafeSample sample = new ThreadSafeSample(); Thread threadA = new Thread(){ public void run(){ sample.nonSafeAction(); } }; Thread threadB = new Thread(){ public void run(){ sample.nonSafeAction(); } }; threadA.start(); threadB.start(); threadA.join(); threadB.join(); } } 2.运行结果:未输出不相等的情况 java ThreadSafeSample 3.jmap反编译代码结果,可以看到其利用monitorenter/monitorexit对,实现同步语义 11: astore_1 12: monitorenter 13: aload_0 14: dup 15: getfield #2 // Field sharedState:I 18: dup_x1 … 56: monitorexit 4.如果synchronized修饰静态方法,可以使用synchronized(className.class){}将代码块包裹起来,或者在静态方法添加 synchronized
-
ReentranLock再入锁实现公平性
public class ThreadSafeSample { private ReentranLock fairLock = new ReentranLock(true); public int sharedState; public void nonSafeAction() { while (sharedState < 100000) { try{ lock.lock(1000,TimeUnit.Seconds); int former = sharedState++; int latter = sharedState; }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); //锁在finally中要释放 } if (former != latter - 1) { System.out.printf("Observed data race, former is " + former + ", " + "latter is " + latter); } } } public static void main(String[] args) throws InterruptedException { ThreadSafeSample sample = new ThreadSafeSample(); Thread threadA = new Thread(){ public void run(){ sample.nonSafeAction(); } }; Thread threadB = new Thread(){ public void run(){ sample.nonSafeAction(); } }; threadA.start(); threadB.start(); threadA.join(); threadB.join(); } }
-
锁的公平性
1.所谓公平性,指竞态环境中,当公平性位置,会赋予等待时间最久的线程。 2.目的是减少线程"饥饿"(个别线程长期等待锁,但无法获取)情况发生。 3.如果使用synchronized时无法保证线程公平性获取锁,用 于不公平,也是主流操作系统调度选择。 4.除非是有公平性要求的场景,通用场景公平不那么重要,建议在程序需要公平性时,再指定它。
可见性
- 一个线程修改了某共享变量,该共享变量状态立即被其他线程知晓,通常被理解为将线程本地状态反映到主内存中,volatile负责保证线程间共享变量可见性
有序性
- 保证线程内部串行语义,避免指令重排。volatile变量语句通过指令优化重排到volatile变量前,但jvm内部happen-before机制可以保证volatile变量不会重排到非volatile变量前面
ReentranLock通过条件变量实现多个条件绑定的锁
- 标准类库中的 ArrayBlockingQueue代码
1.获取锁的条件,队列不空时,不满时获取锁,来进行互斥访问,两个条件变量是从同一把再入锁创建。
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
2.通过条件变量实现一把再入锁绑定两个条件,使用特定操作(take方法)时,判断和等待条件满足(获取锁后,判断为空时等待,不为空出队,最后释放锁)
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock; //1.获取锁
lock.lockInterruptibly();
try {
while (count == 0)
//signal和await成对调用非常重要,不然只有await,线程会一直中断
notEmpty.await(); //2.条件(notEmpty)判断为空进入等待
return dequeue();//否则进行出队
} finally {
lock.unlock();//3.出队后释放锁
}
}
3.通过条件变量NotEmpty判断触发enqueue操作,通过signal/await组合,完成条件判断通知等待线程,顺畅完成主体流转。
private void enqueue(E e) {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
//signal和await成对调用非常重要,不然只有await,线程会一直中断
notEmpty.signal(); // 5.通知等待的线程,非空条件已经满足
}