Java的Synchronized的三种用法以及一些问题
我们这里从Synchronized在用法的分类上分别介绍三种使用方式。
- 前提概念
- 从锁类型分类
- 从使用方式分类
- Synchronized修饰的方法
- Synchronized修饰的静态方法
- Synchronized修饰的代码块
- 关于Synchronized的一些问题
- 对三种用法的一个小结
- 使用Synchronized锁一个常量
- Synchronized只能锁对象吗?可以锁基本变量吗?
前提概念
从锁类型分类:
- 类级锁
锁的是当前这个类的所有对象 - 对象锁
锁的是当前实例对象
从用法中分类:
- Synchronized修饰的方法
锁是当前实例对象 - Synchronized修饰的静态方法
锁是当前类的Class对象 - Synchronized修饰的代码块
锁是Synchronized括号里配置的对象
Synchronized修饰的方法
public synchronized void show (){
//TODO
}
这代表着,调用该方法需要获取当前实例对象的对象锁,也就是同一个对象被synchronized修饰的方法的调用会进入同步队列。
注意:
- synchronized需要放在在返回值前,如上在void前。可以在public之前也可以之后
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
- synchronized 关键字不能被继承 。如果子类覆盖了父类的被synchronized修饰的方法,子类是不会继承父类的synchronized关键字的
Synchronized修饰的静态方法
public class SynClass {
public static synchronized void show (){}
}
我们看到该方法是一个静态方法~所以这代表着synchronized修饰的是一个类的一部分。
所以要调用这个静态方法,必须要获得SynClass类的类级锁,也就是SynClass的Class类型对象的对象锁。再通俗点讲就是锁的是SynClass这个类的所有对象
注意:
- synchronized需要放在在返回值前,如上在void前。可以在public 和 static之间或中间,与他两没有关系
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
- synchronized 关键字不能被继承 。如果子类覆盖了父类的被synchronized修饰的方法,子类是不会继承父类的synchronized关键字的
Synchronized修饰的代码块
使用synchronized修饰的代码块的好处就是可以将锁的范围缩小,让锁更加的灵活,减小开销。
同步代码块大致有三种使用途径,可以实现类级锁也可以实现对象锁:
第一种,实现类级锁
public class SynClass {
public static void main(String[] args) {
synchronized(SynClass.class){
//TODO
}
}
}
效果可以说等同于静态方法锁,但是使用代码块的方式可以实现别的类的类级锁,既括号内是别的类的类类型
第二种,实现当前实例对象的对象锁
public class SynClass {
public void show(){
synchronized(this){
//TODO
}
}
}
效果等同于方法锁,锁的是当前实例对象,但要记住this是不能再静态方法内部使用的。
第三种,实现某个对象的对象锁
public class SynClass {
Object lock = new Object();
public void show(){
synchronized(lock){
//TODO
}
}
}
这里定义了一个Object对象lock,转门用来当做锁。这样的好处是缩小了锁的范围,是针对当前类的实例对象中的某个成员做为锁,这样可以减少锁同步的开销,让锁更加的灵活。
关于Synchronized的一些问题
对三种用法的一个小结
- 同步方法锁的是当前实例对象,是类级锁
- 同步静态方法锁的是当前类的Class对象,是类级锁
- 同步代码块可以实现类级锁也可以实现对象锁,是一种灵活的使用方式。
实践角度理解什么是对象锁
public class ObjectSyn {
public static void main(String[] args) {
//一个实例对象
ObjectSyn obj = new ObjectSyn();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
obj.method1(); //执行obj对象的method1方法
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
obj.method2(); //执行obj对象的method2方法
}
},"t2");
t1.start();
t2.start();
}
//方法一
public synchronized void method1(){
AtomicInteger i = new AtomicInteger(0);
while(true){
i.incrementAndGet();
System.out.println(Thread.currentThread().getName() + " Integer is " + i.get());
if(i.get() == 100)
break;
}
}
//方法二
public synchronized void method2(){
AtomicInteger i = new AtomicInteger(0);
while(true){
i.incrementAndGet();
System.out.println(Thread.currentThread().getName() + " Integer is " + i.get());
if(i.get() == 100)
break;
}
}
}
结果:
t2 Integer is 95
t2 Integer is 96
t2 Integer is 97
t2 Integer is 98
t2 Integer is 99
t2 Integer is 100
t1 Integer is 1
t1 Integer is 2
t1 Integer is 3
t1 Integer is 4
t1 Integer is 5
t2线程执行完毕后才轮到t1线程执行,因为这里仅仅实例化了一个对象obj,虽然执行的是obj对象的不同方法,但是t1和t2线程需要获得的都是obj这把锁,所以是同步执行的。如果我们定义另一个obj2对象,在线程t2中修改为obj2.method2.则整个结果则是异步的。
使用Synchronized锁一个常量
public class SynString {
public static void main(String[] args) {
Thread t1= new Thread(new Runnable() {//线程t1
@Override
public void run() {
SynString.show("a"); //对字符串对象赋值一个字符串常量 等同String str = "a"
}
},"t1");
Thread t2= new Thread(new Runnable() {//线程t2
@Override
public void run() {
SynString.show("a");
}
},"t2");
t1.start();
t2.start();
}
public static void show(String str){ //测试的同步方法
synchronized(str){ //锁的是一个字符串对象
AtomicInteger i = new AtomicInteger(0);
while(true){
i.incrementAndGet();
System.out.println(Thread.currentThread().getName() +" "+ i.get());
if(i.get() == 100)
break;
}
}
}
}
测试结果:
...
t2 96
t2 97
t2 98
t2 99
t2 100
t1 1
t1 2
t1 3
t1 4
...
线程t2执行完才到线程t1执行
这是为什么呢?这是因为"a"是一个字符串常量,是存储在字符串常量池中的。虽然赋予给不同的变量,但是这个对象任然是常量池中的同一个对象。所以也就是同一个对象锁。t2线程在获取锁的时候自然t1线程需要等待t2线程释放锁才能获得该锁
如果我们要想要获得不同对象锁,那么我们要怎么修改呢?
Thread t1= new Thread(new Runnable() {
@Override
public void run() {
SynString.show("a");
}
},"t1");
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
SynString.show(new String("a")); //将"a"修改为new String("a")
}
},"t2");
测试结果:
...
t2 73
t1 87
t2 74
t1 88
t2 75
t1 89
t2 76
t1 90
t2 77
t1 91
...
我们只需要重写new一个字符串对象既可,因为"a"是存储在字符串常量池中,但是new String(“a”),则是根据常量池的"a"对象在堆中的一个新的拷贝,内存位置是不一样的。所以自然获取的对象锁是不一样的,也不会造成同步。注意如果是new String(“a”).intern()则依然获得的是同一个对象锁
同理,还有Integer等其他设计有常量池技术的类型都存在这样的问题。所以一般情况下,Synchronized不修饰常量对象,避免出错
Synchronized只能锁对象吗?可以锁基本变量吗?
public class SynObject {
public void show(int i){
synchronized(i){ //error,int is not a valid type's argument for the synchronized statement
}
}
}
很明显…这是不可以的~那么为什么呢?我们这里引用一个网站回答的答案:
remoteJavaSky 写道:
如果基本类型可以被锁,那么它就需要有一个锁对象与之关联(在方法执行的时候会获取或释放),还需要一个wait集合与之关联(wait,notify,notifyall使用) 那这个基本类型还算是基本类型了吗?并且已经有了基本类型的包装了,这个不是问题了吧我想
我觉得是非常有道理的~同时在《Java并发编程艺术》和《深入理解Java虚拟机》原理两书中有提到一个概念,叫对象头,而对象头的Mark Word区域存放了锁的信息。也就是说这个对象头的Mark Word区域存放了对象锁的信息,比如谁获得了这个锁,目前这个锁是什么锁等等而基本类型必然是没有这个对象头,不然那就叫对象了。所以理由不言而喻了~