Java-synchronized关键字
- synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。
- synchronized关键字是一种独占式的悲观锁,同时属于可重入锁。
上面的关键词:
- 悲观锁:每次读取数据时都认为别人会修改数据,所以每次读写数据时都会加锁,这样导致其他线程想要读写这个数据时就会阻塞、等待直到获取锁。
Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
synchronized的作用范围
了解作用范围能帮助我们知道多线程情况下何时才能获取到被加synchronized关键字的资源。
作用范围如下:
- 作用于成员变量和非静态方法,锁住的是对象的实例,即this对象
- 作用于静态方法,锁住的是Class实例,因为静态方法属于Class而不属于对象
- 作用于代码块,锁住的是所有代码块中配置的对象
为了详细了解synchronized关键字作用于非静态方法和作用于静态方法的区别,我们定义如下的对象,其中有两个非静态方法和两个静态
package com.tianhao.luo.thread.concurrent;
/**
* Synchronized关键字
*
* @author: tianhao.luo@hand-china.com 2021/5/4 16:44
*/
public class SynchronizedDemo {
/**
* synchronized作用于非静态方法
*/
public synchronized void generalMethod1() {
try {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod1: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void generalMethod3(){
try {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod3: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
作用于成员变量和非静态方法
实例一 不同线程竞争同一个对象
定义一个Junit测试类如下:
public class SynchronizedDemoTest extends TestCase {
private final CountDownLatch countDownLatch = new CountDownLatch(2);
public void testFieldAndNotStaticMethod1() throws Exception{
// 测试synchronized作用于非静态方法generalMethod1
// 两个线程中都是用了同一个synchronizedDemo对象调用方法generalMethod1
// 此时被加锁的是synchronizedDemo对象,导致只能是先获取到synchronizedDemo对象的线程先跑完整个循环
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
synchronizedDemo.generalMethod1();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
synchronizedDemo.generalMethod1();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
执行输出的结果如下:测试方法中启动了两个子线程,Thread-1的输出会等待Thread-0中输出完毕后才出现。
=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-0,exec generalMethod1: 1
Thread-0,exec generalMethod1: 2
Thread-1,exec generalMethod1: 0
Thread-1,exec generalMethod1: 1
Thread-1,exec generalMethod1: 2
=========this is step 3=========
总结:step 2
在Thread-0还没有输出完毕就输出了,表示已经进入了Thread-1线程;Thread-0和Thread-1中使用的都是同一个synchronizedDemo对象,因为synchronized是悲观锁,作用于非静态方法,锁住的是当前对象;导致当第二个线程中synchronizedDemo对象执行非静态方法还需要等待第一个线程中synchronizedDemo对象执行非静态方法完毕释放掉monitor。
实例二 不同线程使用不同的SynchronizedDemo对象
定义的测试方法与上面不同的是创建了两个SynchronizedDemo类对象,并且两个线程分别调用不同对象的加了synchronized方法。
public class SynchronizedDemoTest extends TestCase {
private final CountDownLatch countDownLatch = new CountDownLatch(2);
public void testFieldAndNotStaticMethod2() throws Exception{
// 测试synchronized作用于非静态方法generalMethod1
// 两个线程分别执行不同的synchronizedDemo对象的generalMethod方法
// 此时第一个线程被加锁的是synchronizedDemo1对象,第二个线程被加锁的是synchronizedDemo2对象,这两个线程之间不存在对synchronizedDemo对象的竞争
SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
synchronizedDemo1.generalMethod1();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
synchronizedDemo2.generalMethod1();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。
=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-1,exec generalMethod1: 0
Thread-0,exec generalMethod1: 1
Thread-1,exec generalMethod1: 1
Thread-1,exec generalMethod1: 2
Thread-0,exec generalMethod1: 2
=========this is step 3=========
总结:此处的输出恰好验证了synchronized关键字对非静态方法的加锁是针对对象的,不同线程对不同引用对象加的锁互相不会干扰。
作用于静态方法
实例一 不同线程竞争同一个Class对象
测试方法中两个线程都直接使用SynchronizedDemo类去调用静态方法
public class SynchronizedDemoTest extends TestCase {
private final CountDownLatch countDownLatch = new CountDownLatch(2);
public void testFieldAndStaticMethod1() throws Exception{
// 测试synchronized作用于静态方法generalMethod3
// 两个线程都是使用的SynchronizedDemo类去直接调用static方法
// 此时被加锁的是SynchronizedDemo这个Class对象,导致只能是先获取到SynchronizedDemo的Class对象的线程先跑完整个循环
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
SynchronizedDemo.generalMethod3();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
SynchronizedDemo.generalMethod3();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
执行的输出结果如下:Thread-0线程输出完毕后,Thread-1线程才开始输出;和两个线程中都使用同一个synchronizedDemo对象调用synchronized非静态方法的输出相似。
=========this is step 1=========
=========this is step 2=========
Thread-0,exec generalMethod3: 0
Thread-0,exec generalMethod3: 1
Thread-0,exec generalMethod3: 2
Thread-1,exec generalMethod3: 0
Thread-1,exec generalMethod3: 1
Thread-1,exec generalMethod3: 2
=========this is step 3=========
总结:synchronizaed作用于静态同步方法,锁住的是当前类的Class对象,也就是说当多个线程同时调用类的synchronized静态方法时,会形成阻塞。
实例二 不同线程使用SynchronizedDemo对象和Class对象
public class SynchronizedDemoTest extends TestCase {
private final CountDownLatch countDownLatch = new CountDownLatch(2);
public void testFieldAndStaticMethod2() throws Exception{
// 测试synchronized作用于非静态方法generalMethod1和静态方法generalMethod3
// 一个线程使用的SynchronizedDemo类去直接调用static方法,一个线程使用synchronizedDemo对象调用非静态方法
// 第一个线程被加锁的是synchronizedDemo这个对象,第二个线程被加锁的是SynchronizedDemo这个Class对象;两个线程之间不存在竞争对象的关系
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
synchronizedDemo.generalMethod1();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
SynchronizedDemo.generalMethod3();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。和不同线程使用不同的SynchronizaedDemo对象调用synchronized非静态方法的输出一致。
=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-1,exec generalMethod3: 0
Thread-0,exec generalMethod1: 1
Thread-1,exec generalMethod3: 1
Thread-0,exec generalMethod1: 2
Thread-1,exec generalMethod3: 2
=========this is step 3=========
总结:SynchronizedDemo类对象的锁和SynchronizedDemo的Class对象的锁是互不干扰的。
总结
- 不同线程(T1,T2)同时调用对象©被synchronized修饰的非静态方法(M)时,会形成对对象©锁的竞争关系
- 不同线程(T1,T2)同时调用类中©被synchronized修饰的静态方法(M)时,会形成对类Class对象©锁的竞争关系
- 不同线程(T1,T2)同时调用同类但不同对象(C1,C2)被synchronized修饰的非静态方法(M)时,不同对象(C1,C2)的锁互不干扰
- 不同线程(T1,T2)同时调用对象(C1)被synchronized修饰的非静态方法(M1)和调用类中(C2)被synchronized修饰的静态方法(M2),类对象(C2)和Class对象(C1)的锁互不干扰
作用于代码块
作用于代码块时,锁住的是synchronized中的对象,这个被加锁的对象有多种情况。如:
- this对象
- 常量
- 变量/对象
定义一个类如下:其中四个方法都是synchronized加锁代码块,不过锁的对象不一样:
- generalMethod6方法中锁的是this,即当前对象引用
- generalMethod7方法中锁的是integer1,即常量
- generalMethod8方法中锁的是integer2,即变量
- generalMethod9方法中锁的是integer3,即对象
package com.tianhao.luo.thread.concurrent;
/**
* Synchronized关键字
*
* @author: tianhao.luo@hand-china.com 2021/5/4 16:44
*/
public class SynchronizedDemo {
//装箱动作是通过valueOf方法实现的。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获取对象,不在该范围的int类型则通过new生成包装对象。
Integer integer1 = 127;
Integer integer2 = 130;
Integer integer3 = new Integer(124);
public void generalMethod6(){
try {
synchronized (this){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod6: " + i);
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void generalMethod7(){
try {
synchronized (integer1){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod7: " + i);
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void generalMethod8(){
try {
synchronized (integer2){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod8: " + i);
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void generalMethod9(){
try {
synchronized (integer3){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod9: " + i);
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
实例一 代码块加锁this
测试方法结构如下:
public class SynchronizedDemoTest extends TestCase {
public void testCodeBlock3() throws Exception{
// 测试synchronized作用于generalMethod5方法中的代码块
// 两个线程用不同synchronizedDemo对象调用加synchronized修饰代码块的同一方法
// 两个线程中被加锁的是synchronizedDemo这个对象的引用this;两个线程之间加锁对象不同,互不干扰
SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
synchronizedDemo1.generalMethod6();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
synchronizedDemo2.generalMethod6();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。
=========this is step 1=========
Thread-0,exec generalMethod6: 0
=========this is step 2=========
Thread-1,exec generalMethod6: 0
Thread-1,exec generalMethod6: 1
Thread-0,exec generalMethod6: 1
Thread-0,exec generalMethod6: 2
Thread-1,exec generalMethod6: 2
=========this is step 3=========
实例二 代码块加锁常量
将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod7(),即加锁为定义的成员变量(Integer integer1 = 127;
),实际上由于装箱的原理,当使用=
创建范围为-128~127
的Integer对象时,是取自常量池,所以这里可以理解为加锁常量。
执行输出的结果如下:Thread-1会等待Thread-0执行完毕,虽然是不同的对象调用方法,但是由于加锁的是常量,导致两个线程形成了对常量的竞争。
=========this is step 1=========
Thread-0,exec generalMethod7: 0
=========this is step 2=========
Thread-0,exec generalMethod7: 1
Thread-0,exec generalMethod7: 2
Thread-1,exec generalMethod7: 0
Thread-1,exec generalMethod7: 1
Thread-1,exec generalMethod7: 2
=========this is step 3=========
实例三 代码块加锁变量
将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod8(),即加锁为定义的成员变量(Integer integer2 = 130;
),实际上由于装箱的原理,当使用=
创建范围不为-128~127
的Integer对象时,使用了new生成,所以这里可以理解为加锁变量。
执行的输出如下:Thread-1和Thread-0交替输出,即两个线程中加锁的对象不同。
=========this is step 1=========
Thread-0,exec generalMethod8: 0
=========this is step 2=========
Thread-1,exec generalMethod8: 0
Thread-0,exec generalMethod8: 1
Thread-1,exec generalMethod8: 1
Thread-0,exec generalMethod8: 2
Thread-1,exec generalMethod8: 2
=========this is step 3=========
将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod9(),即加锁为定义的成员变量(Integer integer3 = new Integer(124);
),使用new关键字创建的对象会指向不同的引用,所以这里可以理解为加锁变量。
执行的输出如下::Thread-1和Thread-0交替输出,即两个线程中加锁的对象不同。
=========this is step 1=========
Thread-0,exec generalMethod9: 0
=========this is step 2=========
Thread-1,exec generalMethod9: 0
Thread-1,exec generalMethod9: 1
Thread-0,exec generalMethod9: 1
Thread-1,exec generalMethod9: 2
Thread-0,exec generalMethod9: 2
=========this is step 3=========
总结
synchronized作用于代码块时,加锁的对象不同,不同线程之间的竞争关系也不同
- 加锁对象为this时,如果是多线程同时调用同一对象该方法时,多线程会竞争该对象。如果是多线程同时调用不同对象该方法时,由于加锁对象不同,则无竞争关系
- 加锁对象为常量时,如果是多线程同时调用该方法时(无论是否是不同的对象),多线程都会竞争该常量对象,形成阻塞
- 加锁对象为变量时,和this一致
死锁的形成
定义类如下:其中generalMethod20方法中不仅会先对integer1加锁,还会对integer4加锁。generalMethod21方法加锁顺序恰好和generalMethod20相反。
public class SynchronizedDemo {
//装箱动作是通过valueOf方法实现的。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获取对象,不在该范围的int类型则通过new生成包装对象。
Integer integer1 = 127;
Integer integer4 = 126;
public void generalMethod20(){
try {
synchronized (integer1){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod20: " + i);
Thread.sleep(1000);
}
synchronized (integer4){
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void generalMethod21(){
try {
synchronized (integer4) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ",exec generalMethod21: " + i);
Thread.sleep(1000);
}
synchronized (integer1){
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试方法如下:
public class SynchronizedDemoTest extends TestCase {
public void testCodeBlock7() throws Exception{
// 测试synchronized作用于generalMethod方法中的代码块
// 两个线程用不同synchronizedDemo对象分别调用两个方法去获取对方还未释放的锁对象,因为互相等待形成死锁
SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 1=========");
synchronizedDemo1.generalMethod20();
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("=========this is step 2=========");
synchronizedDemo2.generalMethod21();
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
System.out.println("=========this is step 3=========");
}
}
测试输出结果如下:多线程中调用的不是相同的方法,方法中开始的加锁对象也不同,所以会先交替输出;但是当Thread-1中的方法执行到去获取integer3对象锁时,由于integer3还未被Thread-0释放,所以Thread-1线程会等待;Thread-0线程执行到去获取integer4对象锁时,由于Thread-1还在阻塞状态并未释放integer4对锁,Thread-0线程也会等待。此时由于没有超时机制,导致线程之间互相等待资源释放,而形成死锁。
=========this is step 1=========
Thread-0,exec generalMethod20: 0
=========this is step 2=========
Thread-1,exec generalMethod21: 0
Thread-1,exec generalMethod21: 1
Thread-0,exec generalMethod20: 1
Thread-1,exec generalMethod21: 2
Thread-0,exec generalMethod20: 2