一. 认识synchronized
先看一下如下Demo
public class Test {
public static void main(String[] args) {
Count obj = new Count();//only one object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
class MyThread2 extends Thread {
Count c;
MyThread2(Count c) {
this.c = c;
}
public void run() {
c.printTable(100);
}
}
class MyThread1 extends Thread {
Count c;
MyThread1(Count c) {
this.c = c;
}
public void run() {
c.printTable(5);
}
}
class Count {
//加和不加的打印区别
public synchronized void printTable(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
//睡眠四秒
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
没加 synchronized 关键字打印
5
100
10
200
15
300
20
400
25
500
加了 synchronized 关键字之后的打印
5
10
15
20
25
100
200
300
400
500
1. 理解Java对象中的锁
在理解synchronized
之前,我们先简单理解下锁的概念。在Java中,每个对象都会有一把锁。当多个线程都需要访问对象时,那么就需要通过获得锁来获得许可,只有获得锁的线程才能访问对象,并且其他线程将进入等待状态,等待其他线程释放锁。如下图所示
二. 理解synchronized关键字
根据Sun官文文档的描述,synchronized
关键字提供了一种预防线程干扰和内存一致性错误的简单策略,即如果一个对象对多个线程可见,那么该对象变量(final
修饰的除外)的读写都需要通过synchronized
来完成。
你可能已经注意到其中的两个关键名词:
- 线程干扰(Thread Interference):不同线程中运行但作用于相同数据的两个操作交错时,就会发生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠;
- 内存一致性错误(Memory Consistency Errors):当不同的线程对应为相同数据的视图不一致时,将发生内存一致性错误。内存一致性错误的原因很复杂,幸运的是,我们不需要详细了解这些原因,所需要的只是避免它们的策略。
从竞态的角度讲,线程干扰对应的是Read-modify-write,而内存一致性错误对应的则是Check-then-act。
结合锁和synchronized的概念可以理解为,锁是多线程安全的基础机制,而synchronized是锁机制的一种实现。
三. synchronized的用法
上图中看出 synchronized 可以用来修饰方法 , 静态方法, 代码块
类对象: 说的就是 Test.class 对象
类的实例对象: 比如: test对象 Test test = new Test();
1. 在实例方法中使用synchronized
public class Master {
//主宰的初始血量
private int blood = 1000;
//每次被击打后血量减5
public int decreaseBlood() {
blood = blood - 5;
return blood;
}
//通过血量判断主宰是否还存活
public boolean isAlive() {
return blood > 0;
}
}
public synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}
注意这段代码中的synchronized
字段,它表示当前方法每次能且仅能有一个线程访问。另外,由于当前方法是实例方法,所以如果该对象存在多个实例的话,不同的实例可以由不同的线程访问,它们之间并无协作关系。
然而,你可能已经想到了,如果当前线程中有两个synchronized
方法,不同的线程是否可以访问不同的synchronized
方法呢?
答案是:不能。
这是因为每个实例内的同步方法,能且仅能有一个线程访问。
2. 在静态方法中使用synchronized
public static synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}
与实例方法的synchronized
不同,静态方法的synchronized
是基于当前方法所属的类,即Master.class
,而每个类在虚拟机上有且只有一个类对象。所以,对于同一类而言,每次有且只能有一个线程能访问静态synchronized
方法。
当类中包含有多个静态的synchronized
方法时,每次也仍然有且只能有一个线程可以访问其中的方法。
注意: 从synchronized
在实例方法和静态方法中的应用可以看出,synchronized
方法是否能允许其他线程的进入,取决于synchronized
的参数。每个不同的参数,在同一时刻都只允许一个线程访问。基于这样的认知,下面的两种用法就很容易理解了。
3. 在实例方法的代码块中使用synchronized
public int decreaseBlood() {
synchronized(this) {
blood = blood - 5;
return blood;
}
}
在某些情况下,你不需要在整个方法层面使用synchronized
,毕竟这样的方式粒度较大,容易产生阻塞。此时,在代码块中使用synchronized
就是非常不错的选择,如上面代码所示。
刚才已经提到,synchronized
的并发限制取决于其参数,在上面这段代码中的参数是this
,即当前类的实例对象。而在前面的public synchronized int decreaseBlood()
中,synchronized
的参数也是当前类的实例对象。因此,下面这两段代码是等同的:
public int decreaseBlood() {
synchronized(this) {
blood = blood - 5;
return blood;
}
}
public synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}
4. 在静态方法的代码块中使用synchronized
同理,下面这两个方法的效果也是等同的。
public static int decreaseBlood() {
synchronized(Master.class) {
blood = blood - 5;
return blood;
}
}
public static synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}
四. synchronized小结
前面,我们已经介绍了synchronized
的几种常见用法,不必死记硬背,你只要记住synchronized
可以接受任何非null对象作为参数,而每个参数在同一时刻能且只能允许一个线程访问即可。此外,还有一些具有实际指导意义的Tips你可以注意下:
- Java中的
synchronized
关键字用于解决多线程访问共享资源时的同步,以解决线程干扰和内存一致性问题; - 你可以通过 代码块(code block) 或者 方法(method) 来使用
synchronized
关键字; synchronized
的原理基于对象中的锁,当线程需要进入synchronized
修饰的方法或代码块时,它需要先获得锁并在执行结束后释放它;- 当线程进入非静态(non-static)同步方法时,它获得的是对象实例(Object level)的锁。而线程进入静态同步方法时,它所获得的是类实例(Class level)的锁,两者没有必然关系;
- 如果
synchronized
中使用的对象是null,将会抛出NullPointerException
错误; synchronized
对方法的性能有一定影响,因为线程要等待获取锁;- 使用
synchronized
时尽量使用代码块,而不是整个方法,以免阻塞整个方法; - 尽量不要使用String类型和原始类型作为参数**。这是因为,JVM在处理字符串、原始类型时会对它们进行优化。比如,你原本是想对不同的字符串进行加锁,然而JVM认为它们是同一个,很显然这不是你想要的结果。
五. 转载文章
转载博客: 并发王者课-青铜04:宝刀屠龙-如何使用synchronized之初体验 - 简书