Java中synchronized关键字原理和用法 - Java多线程系列教程三
在java中synchronized关键字是同步锁,同步锁是依赖于对象而存在的,而且每一个对象有且仅有一个同步锁。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
synchronized原理
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。
1. monitor锁定过程
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
a、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
b、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
c、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
2. synchronized锁
Java SE1.6对Synchronized进行了各种优化之后,它并不那么重了。在不同的场景中引入不同的锁优化。
1.偏向锁:适用于锁没有竞争的情况,假设共享变量只有一个线程访问。如果有其他线程竞争锁,锁则会膨胀成为轻量级锁。
2.轻量级锁:适用于锁有多个线程竞争,但是在一个同步方法块周期中锁不存在竞争,如果在同步周期内有其他线程竞争锁,锁会膨胀为重量级锁。
3.重量级锁:竞争激烈的情况下使用重量级锁。
偏向锁和轻量级锁之所以会在性能上比重量级锁是因为好,本质上是因为偏向锁和轻量级锁仅仅使用了CAS。
3. synchronized锁优化
尽量采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比来选取不通的锁类型进行优化:
锁 |
优点 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
适用于只有一个线程访问同步块场景。 |
轻量级锁 |
竞争的线程不会阻塞,提高了程序的响应速度。 |
如果始终得不到锁竞争的线程使用自旋会消耗CPU。 |
追求响应时间。 同步块执行速度非常快。 |
重量级锁 |
线程竞争不使用自旋,不会消耗CPU。 |
线程阻塞,响应时间缓慢。 |
追求吞吐量。 同步块执行速度较长。 |
synchronized用法
1. 给一个代码块上锁
synchronized可以上锁、解锁。但是它本身并不是锁,它使用的锁来自于一个对象:任何对象实例都有一把内部锁,只有一把。synchronized不仅仅可以对整个method上锁,还可以对method内的某个代码块上锁。
//这个用法就是使用了obj的锁,来锁定一个代码块。 synchronized(obj){ // some code... }
//对整个方法上锁 public synchronized void aMethod(){ // some code... }
这个时候它使用的是当前实例this的锁,相当于下面的模式:
public void aMethod(){ synchronized(this){ // some code... } }
synchronized方法和synchronized代码块例子
public class SynchronizedDemo4 { public synchronized void synMethod() { for(int i=0; i<1000000; i++) ; } public void synBlock() { synchronized( this ) { for(int i=0; i<1000000; i++) ; } } public static void main(String[] args) { Demo4 demo = new Demo4(); long start, diff; start = System.currentTimeMillis(); // 获取当前时间(millis) demo.synMethod(); // 调用“synchronized方法” diff = System.currentTimeMillis() - start; // 获取“时间差值” System.out.println("synMethod() : "+ diff); start = System.currentTimeMillis(); // 获取当前时间(millis) demo.synBlock(); // 调用“synchronized方法块” diff = System.currentTimeMillis() - start; // 获取“时间差值” System.out.println("synBlock() : "+ diff); } }
(某一次)执行结果:
synMethod() : 11 synBlock() : 3
1. “synchronized方法”是用synchronized修饰方法,而 “synchronized代码块”则是用synchronized修饰代码块。
2. synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。
3. synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。
2. 两个代码块的互斥
一个代码块,被上了锁,就无法同时接纳多个线程的访问。如果是2个不同的代码块,都被上了锁,它们之间是否会有影响呢?请看下面的代码:
class SyncData { public void do1() { synchronized(this) { for (int i=0; i < 4; i++) { System.out.println(Thread.currentThread().getName() + "-do1-" + i); try{ Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } } } } public void do2() { synchronized(this) { for (int i=0; i < 4; i++) { System.out.println(Thread.currentThread().getName() + "-do2-" + i); try{ Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } } } } }
创建1个SyncData的实例,开启2个线程,一个线程调用实例的do1方法,另一个线程调用实例的do2方法,你会看到他们之间是互斥的——即使2个线程访问的是实例的不同的方法,依然不能同时访问。因为决定是否可以同时访问的不再是门,而是锁。只要使用的是相同的对象锁,就会互斥访问。
3. synchronized run方法的锁定
public class TestSychronized { static TestSychronized instance = new TestSychronized(); public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public synchronized void run() { for(int i=1; i<4; i++) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread1 still alive, " + i); } } }); new Thread(thread1).start(); new Thread(thread1).start(); } }
如果加了synchronized当前线程取完所有数据后,才会释放锁,输出结果是有序的:
Thread1 still alive, 1 Thread1 still alive, 2 Thread1 still alive, 3 Thread1 still alive, 1 Thread1 still alive, 2 Thread1 still alive, 3
总结
1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
版权声明:本文为JAVASCHOOL原创文章,未经本站允许不得转载。