目录:
- 什么是synchronized?
- synchronized和原子性、可见性和有序性之间的关系
- synchronized的几种用法
- synchronized与lock的区别
1. 什么是synchronized
synchronized中文意为:同步的,同步化的。是Java中的一个关键字。
常用作给方法或者代码块加锁。加锁后,同一时刻只能有一个线程执行这段代码。以此来保证线程安全。
2. synchronized和原子性、可见性和有序性之间的关系
先简单理解下3个概念,
- 原子性(Atomic):
原子(atom)本意指化学反应不可再分的基本微粒。
在编程中原子性指一个操作不可再被分隔成多步。一个操作或者多个操作 要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。 - 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:即程序的执行顺序按照代码的先后顺序执行。
为什么程序的执行顺序有时会不按照代码的先后顺序执行呢?
这里面涉及到指令重排序(Instruction Reorder)的概念。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序的结果不会影响到单线程的执行,但不能保证多线程并发执行时不受影响。
例如以下代码在未发生指令重排序时,其执行顺序为1->2->3->4。但在真正执行时,将可能变为1->2->4->3或者2->1->3->4或者其他。但其会保证1处于3之前,2处于4之前。所有最终结果都是
a=10; b=20
。int a = 0;//语句1 int b = 1;//语句2 a = 10; //语句3 b = 20; //语句4 复制代码
但如果是多线程情况下,另一个线程中有以下程序。当上述的执行顺序被重排序为1->2->4->3,当线程1执行到第3步
b=20
时,切换到线程2执行,其会输出a此时已经是10了
,而此时a的值其实还是为0。if(b == 20){ System.out.print("a此时已经是10了"); } 复制代码
2.1 synchronized与原子性的关系?
被synchronized关键字包裹起来的方法或者代码块可以认为是原子的。因为在锁未释放之前,这段代码无法被其他线程访问到,所以从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。
在Java中,synchronized对应着两个字节码指令
monitorenter
和monitorexit
。通过monitorenter
和monitorexit
指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
2.2 synchronized是如何保证可见性的?
根据JMM(Java Memory Model,Java内存模型)机制,内存主要分为主内存和工作内存两种,线程工作时会从主内存中拷贝一份变量到工作内存中。
JMM对synchronized做了2条规定:
- 线程解锁前,必须把变量的最新值刷新到主内存中。
- 线程加锁时,先清空工作内存中的变量值,从主内存中重新获取最新值到工作内存中。
2.3 synchronized可以保证有序性吗?
synchronized可以保证一定程度的有序性,但其是不能禁止指令重排序的,synchronized 代码块里的非原子操作依旧可能发生指令重排。
具体怎么理解呢?
- 这里要先说一个概念,
as-if-serial语义
,其是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义
。 as-if-serial语义
保证了单线程中指令重排序是有一些限制的,即无论怎么重排序,都不能影响到单线程执行的结果。而synchronized保证了这一块程序在同一时间内只能被同一线程访问,所以其也算是保证了有序性。
3. synchronized的几种用法
synchronized的用法大概可以分为3种,
- 用来修饰普通方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁
- 用来修饰静态方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁
- 作用于代码块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
其作用在方法上的写法如下图,synchronized
只要放在返回类型
前面就行。
下面将举一些具体的实例,来看下使用synchronized
后的结果:
3.1 synchronized作用于实例方法
public class TestBean {
//TestBean中有两个实例方法,method1和method2
public synchronized void method1(){
System.out.println("method1 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method1 end");
}
public synchronized void method2(){
System.out.println("method2 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method2 end");
}
}
复制代码
public class MainTest {
public static void main(String[] args){
TestBean testBean = new TestBean();
//第一个线程,执行method1()
new Thread(new Runnable() {
@Override
public void run() {
testBean.method1();
}
}).start();
/立刻开启第二个线程,执行method2()
new Thread(new Runnable() {
@Override
public void run() {
testBean.method2();
}
}).start();
}
}
复制代码
控制台结果:
method1 start
(...3秒后输出)
method1 end
method2 start
(...3秒后输出)
method2 end
复制代码
可以看出锁作用于testBean
对象上,其他线程来访问synchronized
修饰的其他方法时需要等待线程1先把锁释放。
- 那如果
method2()
的synchronized
修饰符去掉呢。那自然是不用等锁释放,就会立刻执行method2()
。控制台输出以下结果:
method1 start
method2 start
(...3秒后输出)
method1 end
method2 end
复制代码
- 又或者TestBean.java类保持不变,两个方法均由
synchronized
修饰,但有两个不同的实例对象。
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
//第一个线程,执行method1()
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method1();
}
}).start();
/立刻开启第二个线程,执行method2()
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method2();
}
}).start();
}
}
复制代码
此时因为两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响,控制台的结果如下:
method1 start
method2 start
(...3秒后输出)
method1 end
method2 end
复制代码
3.2 synchronized修饰静态方法
将上文中的TestBean.java改为:
public class TestBean {
//TestBean中有一个静态方法,method
synchronized public static void method(String threadName){
System.out.println("method start by " + threadName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method end by " + threadName);
}
}
复制代码
Main.java如下:
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method("thread1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method("thread2");
}
}).start();
}
}
复制代码
控制台结果:
method start by thread1
(...3秒后输出)
method end by thread1
method start by thread2
(...3秒后输出)
method end by thread2
复制代码
分析:由例子可知,两个线程虽然使用的是两个不同的对象,但是访问的方法是静态的,两个线程最终还是发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。
3.3 synchronized修饰代码块
为什么要作用于代码块呢?
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,毕竟长锁不如短锁,尽可能只锁必要的部分。
public class TestBean {
private final static Object objectLock = new Object();
void method1(){
System.out.println("not synchronized method1");
synchronized (objectLock){
System.out.println("synchronized method1 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized method1 end ");
}
}
void method2(){
System.out.println("not synchronized method2" );
synchronized (objectLock){
System.out.println("synchronized method2 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized method2 end");
}
}
}
复制代码
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method2();
}
}).start();
}
}
复制代码
控制台输出:
not synchronized method1
synchronized method1 start
not synchronized method2
(...3秒后输出)
synchronized method1 end
synchronized method2 start
(...3秒后输出)
synchronized method2 end
复制代码
可以看到未被synchronized包裹的代码时不存在互斥的,System.out.println("not synchronized method2" );
无需等到method1执行完成,而被synchronized包裹的代码块,且使用了同一个对象作为锁的话,那就互斥了。
结论:
- 将
synchronized
作用于一个给定的实例对象objectLock,每次当线程进入synchronized
包裹的代码块时就会要求当前线程持有objectLock实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。 - 当然除了objectLock作为对象外,我们还可以使用
this
对象(代表当前实例)或者当前类的class对象
作为锁,如下代码:
synchronized (this){
System.out.println("synchronized method1 start");
System.out.println("synchronized method1 end");
}
synchronized (TestBean.class){
System.out.println("synchronized method2 start");
System.out.println("synchronized method2 end");
}
复制代码
4. synchronized和lock之间的区别
Lock
是Java语言中的一个接口类,对应的实现类为ReentrantLock
,它们都位于java.util.concurrent.locks
包下,熟悉Java的同学肯定都知道concurrent包下都是用于处理Java多线程问题的类。
4.1 lock的使用
lock的几个常用方法:
lock():获取锁,如果锁被暂用则一直等待
unlock():释放锁
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
ck
lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
复制代码
lock的使用与synchronized作用于代码块时类似:
public class TestBean {
private Lock lock = new ReentrantLock();
void method1(String threadName) {
lock.lock();
try{
System.out.println("method1 start " + threadName);
//耗时操作
...
} finally {
System.out.println("method1 end " + threadName);
lock.unlock();//释放锁
}
}
}
复制代码
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
new Thread("thread1") {
@Override
public void run() {
testBean1.method1(Thread.currentThread().getName());
}
}.start();
new Thread("thread2"){
@Override
public void run() {
testBean1.method1(Thread.currentThread().getName());
}
}.start();
}
}
复制代码
执行结果如下:
method1 start thread1
(...3秒后输出)
method1 end thread1
method1 start thread2
(...3秒后输出)
method1 end thread2
复制代码
注意:使用lock时,需要在finally中释放锁lock.unlock();
,不然可能会造成死锁。
- 将TestBean.java稍微改动,来看下
tryLock()
方法的使用:
public class TestBean {
private Lock lock = new ReentrantLock();
void method1(String threadName) {
if (lock.tryLock()){
try{
System.out.println("method1 start " + threadName);
//耗时操作
...
} finally {
lock.unlock();
}
} else {
System.out.println("我是"+threadName+",有人占着锁,我放弃了");
}
}
}
复制代码
控制台输出:
method1 start thread1
我是thread2,有人占着锁,我放弃了
(...3秒后输出)
method1 end thread1
复制代码
4.2 lock与synchronized的区别
类别 | synchorinzed | lock |
---|---|---|
存在层次 | Java的关键字 | 接口类,由ReentrantLock实现 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,可尝试获得锁tryLock() ,线程可以不用一直等待 |
锁的状态 | 无法判断 | 可判断 |
性能 | 资源竞争不是很激烈的情况下,比较合适的;当同步非常激烈的时候,synchronized的性能会一下子能下降得很快 | 在资源竞争不激烈的情形下,性能稍微比synchronized差点;但其在资源竞争激烈时,可维持常态 |
两者具体的性能测试:www.cnblogs.com/nsw2018/p/5…
结语
之后会写一篇文章描述下synchronizeds的底层原理: 涉及到的概念会有Monitor、monitorenter和monitorexit指令、synchronized的可重入性、synchronized与中断。
另外会写一篇关于volatile的文章:主要涉及Volatile的特性、其实现原理、指令重排序与内存屏障