synchronized关键字
synchronized是我们在java中最简单最常用的加锁方式。1.6后synchronized的优化,使其性能提升,因此很多同步加锁的场景,使用synchronized也是很好的选择。
synchronized的使用
- synchronized可以修饰静态方法
public synchronized static void method()
- 非静态方法
public synchronized void method()
- 代码块
synchronized(obj){}
不管是哪一种修饰,synchronized目的是给对象加锁,在多线程运行时,在调用同步代码时,只有拿到对象锁才能执行同步代码。
当synchronized修饰的非静态方法是对this(该类的对象)上锁。
修饰静态方法则是对当前类对象上锁,注意这里的类对象就是Class对象,不是栈中的类信息。
synchronized(obj)显而易见,就是对括号内的obj上锁。
由于之前看过一些博客对于类加锁的情况描述的不够详细,我们通过一个示例来帮助理解类锁。
public class LockTest {
public static void main(String[] args) throws InterruptedException {
//占用类锁的线程
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
Test.test1();
}
});
//用类 调用静态上锁方法
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
Test.test2();
}
});
//无锁方法
Thread t3=new Thread(new Runnable() {
@Override
public void run() {
Test.test3();
}
});
//调用非静态上锁方法
Thread t4=new Thread(new Runnable() {
@Override
public void run() {
new Test().test4();
}
});
//使用对象调用静态上锁方法
Thread t6=new Thread(new Runnable() {
@Override
public void run() {
new Test().test6();
}
});
t1.start();
//t1先执行 先占用类锁
Thread.sleep(100);
t2.start();
t3.start();
t4.start();
t6.start();
}
static class Test{
public static synchronized void test1(){
System.out.println("test1:类加锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void test2(){
System.out.println("test2:静态同步方法 拿锁");
}
public static void test3(){
System.out.println("test3:非同步方法");
}
public synchronized void test4(){
System.out.println("test4:非静态同步方法 拿锁");
}
public void test6(){
synchronized(this.getClass()){
System.out.println("test6");
}
}
}
}
代码描述
示例中Test类中 test1 test2 是 synchronized修饰的静态方法;test3是个普通非同步方法;test4是synchronized修饰的非静态方法;test6是对this.getClass(即对Test类CLass对象)加锁的方法。
首先t1线程调用test1方法,由于t1中有Thread.sleep(),因此t1会持续占有类锁,其他线程依次启动查看锁的竞争情况。
运行结果
- test3(非同步方法)test4(非静态同步方法) 会立即执行,说明这两个方法并不会去竞争类锁。
- test2(静态同步方法)和test6(对Test类CLass对象加锁方法)会等到test1释放锁后才会执行,说明这两个方法会和test1竞争类锁。
结果说明
- test2(静态同步方法)与test1竞争锁比较好理解,因为两者都是同步静态方法,从test6中我们可以看出同步静态方法实际上就是对类Class对象进行了加锁。
- test4(非静态同步方法)没有去竞争锁是因为两个方法竞争的锁对象是不同的,test4是对对象加锁。
- test3(非同步方法)没有竞争锁,是因为执行这个方法是不需要锁的。所以从这里我们可以看到在java中对象加锁只针对与需要竞争锁的代码块或方法,并不会影响普通非同步方法。
synchronized的加锁解锁
由于synchronized关键字是自动上锁,代码执行完自动解锁,因此我们不用做什么处理。并且当方法或代码块中出现异常也会自动释放锁。
synchronized也支持重入。
如:
synchronized(obj){
synchronized(obj){
}
}
当一个synchronized同步方法调用另一个synchronized同步方法两个方法竞争的同一把锁,在执行第二个方法时不会重新释放锁再加锁,而是直接保持锁,并对锁中的计数器加1。第二个方法执行完计数器减一,等到计数器为0时才会将锁释放。因此再锁重入的期间,其他线程是拿不到锁的。
浅说synchronized锁膨胀
synchronized锁级别是由低到高:偏向锁,轻量级锁,重量级锁
jdk1.6前synchronized的性能是被人诟病的,就是因为当时的synchronized只单纯的使用重量级锁。
偏向锁
当synchronized修饰的方法只有一个线程调用时,对象锁状态为偏向锁。在锁第一次被拥有的时候,对象头记录下偏向线程ID。之后每当方法被调用都会去对比线程id是否发生变化,如果其他线程调用,与其记录的线程id不统一会膨胀为轻量级锁。偏向锁更像是无锁状态。
轻量级锁
多个线程交替调用同步代码时,或存在很短时间的竞争关系,轻量级锁采用cas乐观锁非阻塞+自旋的方式,尝试拿锁,如果拿不到会膨胀为重量级锁。
**轻量锁优势:**在于使线程保持活跃,不会切换线程上下文。
轻量锁缺点 不会让出cpu,如果长时间竞争锁cpu压力会比较大
重量级锁
重量级锁也就是我们普遍认知的锁。拿不到锁的线程会让出cpu阻塞。
重量锁优势: 与轻量锁相对的 ,由于重量锁采用阻塞的方式等待锁释放,会让出cpu。
重量锁缺点: 由于线程阻塞需要唤醒线程线程上下文切换的资源耗费。
总结
轻量级锁不能替代重量级锁,两者都有存在的必要,当需要同步的操作很少,线程上下文切换耗时占用总耗时比较大时,使线程一直保持活跃的状态会省掉更多的资源(所以 automic 适用cas+自旋的方式)。但如果线程上下文切换远小于同步代码执行时间,将未拿到锁的线程阻塞让出cpu是更好地选择。