1.前言
多线程这方面的学习参考的是《多线程编程核心技术》(高洪岩 著)这本书,可以说通过这本书,对多线程方面有了更多的了解,特别是书中对synchronized关键字和Lock锁的介绍方面,用了很大篇幅,在篇幅中也穿插了很多例子帮助理解,其中有一个结论当初记得特别深刻,那就是synchronized修饰静态方法和修饰普通方法时的区别。
2.困惑
第一遍学习时,只能说大概记住了结论就是synchronized修饰普通方法时,锁对象为当前实例对象(this), 而当synchronized修饰静态方法时,锁对象为当前类的字节码文件对象。当初只记住了结论是这样子,甚至连这个结论中提及的某些名词都还是很含糊的,例如当前实例对象指的是什么,当前类的字节码文件对象指的是什么,这个结论在代码中什么情况下能体现出区别。
3.后续的解惑
在后来,慢慢通过在其他方面的学习,再回过头来看这个结论,就觉得,其实有些知识的相互融汇贯通的。在学习了《Java并发编程的艺术》(方腾飞 著)一书后,得到的结论是,利用synchronized实现同步的基础是:Java中的每一个对象都可以作为锁,当一个线程试图访问同步方法和同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
在这个结论的基础上,我个人的理解是,这里所说的锁,对于用synchronized修饰方法而言,便是与线程要操作的资源类相关的对象。通常我们会将线程要操作的资源封装在一个类中,然后通过一个公共方法使得在线程的执行体中(run()方法的方法体)利用资源类的相关对象调用这个公共方法,如果这个公共方法时非静态的,则是通过资源类的实例对象来调用这个公共方法,如果这个公共方法时静态的,则是通过资源类直接调用这个方法,更确切来说,是通过资源类的字节码对象调用的。
所以,当调用synchronized修饰的普通方法时,首先必须得到的锁是在线程的执行体中调用资源类公共方法的实例对象的对象锁。用具体代码来看的话,比如下面的代码:
很显然,ThreadDemo为我们的资源类,method1()为我们操作这个资源类所提供的普通方法,于是在main方法中,线程1的执行体里,我们操作ThreadDemo这个资源类,是通过demo这个ThreadDemo类的实例来调用method1()方法以达到操作ThreadDemo资源类的目的,所以在线程1执行的过程中,当它想进入method1()方法前,必须首先获得demo这个对象的对象锁,如果成功获得demo对象的对象锁,则成功进入方法体,否则就处于阻塞状态,等待下一次获得cpu执行的时间片,处于运行状态时再尝试获取demo对象锁。
同理,通过静态方法操作资源类时,也必须首先获得锁,而因为静态方法时属于类而不属于对象的,所以调用静态方法时首先要获取的是资源类的"类锁",也就是字节码对象的对象锁。(注意:即使静态方法通过对象中的某一个实例可以调用,但在实际内存区域中,静态方法时属于整个资源类所有对象所共享的,依然需要首先获得的是字节码对象的对象锁)
4.具体代码体现
同一个锁对象在同一时刻只能被一个线程持有,若其他线程没有获得锁对象,则会阻塞,也就是程序中多个线程之间串行执行,如果多个线程通过不同的方法操作同一资源时,若方法之间不是同样的对象锁,则程序中多个线程之间是并行执行的,看以下3种情况:
(1)线程1和线程2通过同一资源类对象调用两个不同的synchronized修饰的普通方法
/**
* 资源类
*/
class ThreadDemo{
public synchronized void method1(){
System.out.println(Thread.currentThread().getName() + "进入了方法1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开了方法1");
}
public synchronized void method2(){
System.out.println(Thread.currentThread().getName() + "进入了方法2");
System.out.println(Thread.currentThread().getName() + "离开了方法2");
}
}
/**
* 操作资源的线程所在的类
*/
public class SynchronizedStaticTest {
public static void main(String[] args) {
final ThreadDemo demo = new ThreadDemo();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method1();
}
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method2();
}
}
},"线程2").start();
}
}
运行结果:
在线程1和线程2的执行体中,通过同一个资源类对象(demo对象)来操作资源类,此时线程1和线程2都必须先得到demo对象的对象锁,而同一时刻demo对象锁只能被1个线程持有,所以运行结果为线程1和线程2呈串行执行,只有等一个线程执行完了操作资源的方法体(本例中为method1()或者method2())之后,另一个线程才能获得对象锁,再执行其run()执行体中操作资源的方法(本例中为method1()或者method2())。也就是说当线程1执行method1时,线程1持有了demo对象锁,线程2必须等待线程1执行完method1,释放了demo对象锁,然后线程2成功获取到demo对象锁之后,才能进入method2执行,method2执行的时候,线程1同理,等待线程2执行完之后释放demo对象。
(2)线程1和线程2通过同一资源类对象调用两个不同的synchronized修饰的方法,一个为普通方法(本例中为method1()),一个为静态方法(本例中为method2())
/**
* 资源类
*/
class ThreadDemo{
public synchronized void method1(){
System.out.println(Thread.currentThread().getName() + "进入了方法1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开了方法1");
}
public static synchronized void method2(){
System.out.println(Thread.currentThread().getName() + "进入了方法2");
System.out.println(Thread.currentThread().getName() + "离开了方法2");
}
}
/**
* 操作资源的线程所在的类
*/
public class SynchronizedStaticTest {
public static void main(String[] args) {
final ThreadDemo demo = new ThreadDemo();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method1();
}
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method2();
}
}
},"线程2").start();
}
}
运行结果:
在线程1和线程2的执行体中,虽然通过了同一个对象(demo)对象来调用操作资源的方法,但由于线程1调用的method1为普通方法,此时线程1需要执行method1前需要获得的锁是demo对象锁,而虽然线程2也通过对象调用了method2方法,但由于method2方法时是静态的,属于ThreadDemo这个类而不属于demo这个对象,所以线程2执行method2前需要获得的锁是ThreadDemo.class对象锁,所以当线程1持有demo对象锁时,不影响线程2去尝试获得ThreadDemo.class对象锁,所以线程1和线程2在本程序的执行呈并行效果,也就是线程1执行method1时,不管method1有没有执行完,线程2只要获得了ThreadDemo.class对象锁,都可以进入method2执行。
(3)线程1和线程2通过同一资源类对象调用两个不同的synchronized修饰的静态方法
/**
* 资源类
*/
class ThreadDemo{
public static synchronized void method1(){
System.out.println(Thread.currentThread().getName() + "进入了方法1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开了方法1");
}
public static synchronized void method2(){
System.out.println(Thread.currentThread().getName() + "进入了方法2");
System.out.println(Thread.currentThread().getName() + "离开了方法2");
}
}
/**
* 操作资源的线程所在的类
*/
public class SynchronizedStaticTest {
public static void main(String[] args) {
final ThreadDemo demo = new ThreadDemo();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method1();
}
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 100; i++){
demo.method2();
}
}
},"线程2").start();
}
}
运行结果:
与情况1同理,此时线程1和线程2都是通过静态方法来操作资源类,所以在执行各自所在操作同一资源类的方法前,都需要首先获得资源类的字节码对象的对象锁(ThreadDemo.class)。此时线程1和线程2都必须先得到字节码对象的对象锁(ThreadDemo.class),而同一时刻字节码对象的对象锁(ThreadDemo.class)只能被1个线程持有,所以运行结果为线程1和线程2呈串行执行,只有等一个线程执行完了操作资源的方法体(本例中为method1()或者method2())之后,另一个线程才能获得对象锁,再执行其run()执行体中操作资源的方法(本例中为method1()或者method2())。也就是说当线程1执行method1时,线程1持有了字节码对象的对象锁(ThreadDemo.class),线程2必须等待线程1执行完method1,释放了demo对象锁,然后线程2成功获取到字节码对象的对象锁(ThreadDemo.class)之后,才能进入method2执行,method2执行的时候,线程1同理,等待线程2执行完之后释放字节码对象的对象锁(ThreadDemo.class)。