目录
目录
1、sychronized关键字修饰普通方法+ 修饰普通代码块实现同步的实例
2、sychronized关键字修饰static方法(静态方法)实现同步的实例
我们知道Java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。
常见的可以使用synchronized
修饰this;
修饰static方法;
或者修饰部分代码块。
效果的不一样主要看synchronized
锁住的
是什么锁。每个类都有自己的锁,类的不同对象有各自的对象锁。
- 锁住的是类的锁:类的锁的管辖范围比对象的锁的管辖范围大,不同的对象之间的锁互不影响,但是他们都受类的锁的控制,如果一个类的锁被一个线程获得,那么这个类的所有的对象都不能访问需要获得类的锁的方法,但是可以访问不需要锁的方法和需要某个对象锁的方法;
- 两个方法是不是互斥:关键是看两个方法取得的锁是不是互斥的,如果锁是互斥的,那么方法也是互斥访问的,如果锁不是互斥的,那么不同的锁之间是不会有什么影响的,所以这时方法是可以同时访问的。
1、sychronized关键字修饰普通方法+ 修饰普通代码块实现同步的实例
1、1 修饰普通方法
用synchronized
修饰普通方法表示整个方法都必须用this
实例加锁。synchronized
锁住的是this
实例的锁,就是拥有这个普通方法的实例的锁,而不是该类的锁。
封装synchronized
逻辑代码块
:让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized
逻辑封装起来。例如,我们编写一个线程安全(如果一个类允许多线程正确访问,则被称为线程安全的,thread-safe)的计数器Counter类如下:
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
这样一来,线程调用add()
、dec()
方法时,它不必关心同步逻辑,因为synchronized
代码块在add()
、dec()
方法内部。并且,我们注意到,synchronized
锁住的对象是this
,即当前实例,这又使得创建多个Counter
实例的时候,它们之间互不影响,可以并发执行:
var c1 = Counter();
var c2 = Counter();
// 对c1进行操作的线程:
new Thread(() -> {
c1.add();
}).start();
new Thread(() -> {
c1.dec();
}).start();
// 对c2进行操作的线程:
new Thread(() -> {
c2.add();
}).start();
new Thread(() -> {
c2.dec();
}).start();
注:
- 当我们锁住的是
this
实例时,实际上可以用synchronized
修饰这个方法。下面两种写法是等价的:
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
- 针对返回多个基本类型数据的方法,是需要同步的!单个不需要:
考察Counter
的get()
方法:
public class Counter {
private int count;
public int get() {
return count;
}
...
}
它没有同步,因为读一个int
变量不需要同步。
然而,如果我们把代码稍微改一下,返回一个包含两个int
的对象,就必须要同步:
public class Counter {
private int first;
private int last;
public Pair get() {
Pair p = new Pair();
p.first = first;
p.last = last;
return p;
}
...
}
1、2 修饰部分代码块 - 同步块
synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是:
synchronized(this){/区块/}
它的作用域是当前对象,这时锁就是对象的锁,谁拿到这个锁谁就可以运行它所控制的那段代码。
当有一个明确的对象作为锁时,就可以这样写程序;
但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象):
class Foo implements Runnable {
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA() {
synchronized(lock) { //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
2、sychronized关键字修饰static方法(静态方法)实现同步的实例
对于static
方法,是没有this
实例的,因为static
方法是针对类而不是实例。但是任何一个类都有一个由JVM自动创建的Class
实例,因此,对static
方法添加synchronized
,锁住的是该类的class
实例。即:
public synchronized static void test(int n) {
...
}
相当于:
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}
3、总结
关于线程安全:
- 通过合理的设计和数据封装可以让一个类变为“线程安全”;
- 一个类没有特殊说明,默认不是thread-safe;
线程安全定义:如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)。
常见线程安全类:
- java标准库的
java.lang.StringBuffer
; - 还有一些不变类,例如
String
,Integer
,LocalDate
,它们的所有成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。 - 最后,类似
Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList
,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList
是可以安全地在线程间共享的。
hashMap也是线程不安全的,这里面涉及到源码,线程安全多关乎一些变量、方法在底层实现中是否满足同步(满足原子性)、满足及时的全局更新等,目前还没深入去看,以后专门写文章来总结。这是之前总结的关于hashMap线程不安全的原因:
4、参考
参考一篇文章,讲述的比较清楚,synchronized修饰方法,包括普通方法和静态方法
https://blog.youkuaiyun.com/xingjiarong/article/details/47907237