Thread - Synchronized

什么是Synchronized

synchronize 同步

使用synchronized关键字修饰的代码块,同一时间只能有一个线程进行访问。 通过加锁,保证多线程访问共享资源时的线程安全

锁的是什么

首先,强调一点:锁是加在对象上面的。 synchronized关键字可以用在各种地方(见下面的作用范围),但不管怎样,最终,是一个对象被加锁了。

需要注意的是,一个对象被加锁了,不代表其它线程就完全不能使用这个对象了。 而是说其他线程无法访问该对象的任何synchronized方法。但是可以调用其他非synchronized的方法。

作用范围

一般说是,可以分为三类:

其实,个人感觉是两类,就看synchronized修饰的是静态的方法/对象,还是非静态的。 与之对应的,加的是类对象锁,还是实例对象锁

public class SynchronizedSample {

   private final Object lock = new Object();

   private static int money = 0;
   
   //静态方法
   public static synchronized void staticMethod(){
	   money++;
   }
   
   //非静态方法
   public synchronized void noStaticMethod(){
	   money++;
   }
   
   public void codeBlock(){	   
	   //代码块
	   synchronized (lock){
		   money++;
	   }
   }
}

作用范围验证

非静态方法锁的是实例对象

首先,我们来验证非静态方法上使用synchronized,加锁的对象是调用这个方法的实例对象。

首先,我们定义了一个非静态方法 minus1()。它会倒计时5秒然后print出来。

public class TestSynchronized {

    public synchronized void minus1() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

然后,我们创建两个线程,创建一个实例化对象,两个线程都调用了这个对象的方法。

public static void main(String[] args) {

	TestSynchronized test1 = new TestSynchronized();
	
	Thread thread1 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus1();
		}
	});

	Thread thread2 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus1();
		}
	});

	thread1.start();
	thread2.start();
}

结果 ->

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

分析:一个线程执行完了,才轮到另一个执行。此时有加锁。

作为对比组,我们创建两个实例化对象。每个线程内各自调用一个实例化对象的方法。

public static void main(String[] args) {

	TestSynchronized test1 = new TestSynchronized();
	TestSynchronized test2 = new TestSynchronized();

	Thread thread1 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus1();
		}
	});

	Thread thread2 = new Thread(new Runnable() {

		@Override
		public void run() {
			test2.minus1();
		}
	});

	thread1.start();
	thread2.start();
}

结果 ->

Thread-0 - 4
Thread-1 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-0 - 0
Thread-1 - 0

分析:两个线程”同时”执行各自实例化对象的方法,此时没有竞争,没有加锁。

总结:非静态方法上使用synchronized,加锁的作用的对象是调用这个方法的实例对象。

锁只作用于synchronized方法,而不是其它方法

再做一个实验,一个类中,有一个synchronized方法,一个普通方法。

public class TestSynchronized {

    public synchronized void minus1() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

两个线程,分别调用一个实例对象的这两个方法。

public static void main(String[] args) {

	TestSynchronized test1 = new TestSynchronized();

	Thread thread1 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus1();
		}
	});

	Thread thread2 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus2();
		}
	});

	thread1.start();
	thread2.start();
}

结果 ->

Thread-0 - 4
Thread-1 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-0 - 0
Thread-1 - 0

分析总结:两个方法并行执行。锁只作用于synchronized方法,而不是其它方法。

如果不信,可以把minus2()加上synchronized关键字,再运行一遍。这里就不演示了。 (结果是一个Thread执行完了,另一个Thread才执行。)

静态方法锁的是类对象

接着测试,一个方法是synchronized,一个方法是static + synchronized

public class TestSynchronized {

    public synchronized void minus1() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized static void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

运行一遍。

public static void main(String[] args) {

	TestSynchronized test1 = new TestSynchronized();

	Thread thread1 = new Thread(new Runnable() {

		@Override
		public void run() {
			test1.minus1();
		}
	});

	Thread thread2 = new Thread(new Runnable() {

		@Override
		public void run() {
			TestSynchronized.minus2();
		}
	});

	thread1.start();
	thread2.start();
}

结果如下:

Thread-0 - 4
Thread-1 - 4
Thread-1 - 3
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-0 - 0
Thread-1 - 0

很神奇,两个并行执行了。原因是静态方法加的是类锁,而非静态方法加的是对象锁。 两个是不一样的。

代码块的特殊情况

代码块的常见情形就不测试了,考虑下面的情况:synchronized修饰了一个对象,且这个对象是在本地方法内创建的。 理论上说,每次运行该方法,都会创造一个新的对象。那锁还有用吗?

public class TestSynchronized {

    //Object obj = new Object(); // common scenario 1
    //static Object obj = new Object(); // common scenario 2

    public void minus3() {
        Object obj = new Object();
        synchronized (obj) {
            int count = 5;
            for (int i = 0; i < 5; i++) {
                count--;
                System.out.println(Thread.currentThread().getName() + " - " + count);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }
        }
    }
	
}

运行一下。

public static void main(String[] args) {

    TestSynchronized test1 = new TestSynchronized();

    Thread thread1 = new Thread(new Runnable() {

        @Override
        public void run() {
            test1.minus3();
        }
    });

    Thread thread2 = new Thread(new Runnable() {

        @Override
        public void run() {
            test1.minus3();
        }
    });

    thread1.start();
    thread2.start();
}

结果 ->

Thread-0 - 4
Thread-1 - 4
Thread-0 - 3
Thread-1 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-0 - 0
Thread-1 - 0

最后并行执行了。这样写其实synchronized关键字就没有任何效果了,加了和没加一样。

这个例子在实际开发中可能不会遇到,但是,对于理解synchronized很有帮助!

发展

在 JDK6 以前,synchronized 还属于重量级锁。

到了 JDK6,引入了偏向锁和轻量级锁。

为啥?因为研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现多个线程竞争一把锁的情况概率比较低。 基于这个现象,引入了偏向锁和轻量级锁,线程采用CAS来获取锁,效率高。但同时,又不会无限自旋。(自旋满一定数量会升级成重量级锁)

参考

Fork me on GitHub