Java 对象及变量的并发访问之 synchronized

168 阅读22分钟

1、synchronized 同步方法

作用:保证原子性、可见性、有序性

1.1 方法内的变量为线程安全

方法中的变量不存在非线程安全问题,永远线程安全,这是由于方法内部的变量具有私有特性

例子:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        PrivateNum privateNum = new PrivateNum();
        ThreadA threadA = new ThreadA(privateNum);
        threadA.start();
        ThreadB threadB = new ThreadB(privateNum);
        threadB.start();
    }
}

class PrivateNum {
    public void addI(String name) {
        try {
            int num = 0;
            if (name.equals("a")) {
                num = 100;
                System.out.println("set a over");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("set b over");
            }
            System.out.println("num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread {

    private PrivateNum privateNum;

    public ThreadA(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("a");
    }
}

class ThreadB extends Thread {

    private PrivateNum privateNum;

    public ThreadB(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("b");
    }
}

输出:

set a over
set b over
num=200
num=100

1.2 实例变量非线程安全问题与解决方法

如果多个线程共同访问一个对象中的实例变量,可能出现非线程安全问题

对于上一节的代码稍作改动,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        PrivateNum privateNum = new PrivateNum();
        ThreadA threadA = new ThreadA(privateNum);
        threadA.start();
        ThreadB threadB = new ThreadB(privateNum);
        threadB.start();
    }
}

class PrivateNum {
    // num变量为类所拥有而不是方法
    private int num = 0;

    public void addI(String name) {
        try {
            if (name.equals("a")) {
                num = 100;
                System.out.println("set a over");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("set b over");
            }
            System.out.println("num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class ThreadA extends Thread {

    private PrivateNum privateNum;

    public ThreadA(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("a");
    }
}

class ThreadB extends Thread {

    private PrivateNum privateNum;

    public ThreadB(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("b");
    }
}

输出:

set a over
set b over
num=200
num=200

解决方法:

public void addI(String name) 修改为 synchronized public void addI(String name)

输出:

set a over
num=100
set b over
num=200

结论:

两个线程访问同一个对象中的同步方法一定是线程安全的。不管哪个线程先运行,线程进入 synchronized 声明的方法就上锁,方法执行完就解锁,之后下一个线程才会进入用 synchronized 声明的方法,当前线程不解锁其他线程执行不了 synchronized 声明的方法。

1.3 同步 synchronized 在字节码指令中的原理

在方法中使用 synchronized 进行同步的原因是使用了 flag 标记 ACC_SYNCHRONIZED ,当调用方法时,调用指令会先检查方法的 ACC_SYNCHRONIZED 访问标志符是否设置,如果设置了,执行线程会持有同步锁,然后执行方法,最后在方法完成时释放锁。

例子:

public class Run {
    public static void main(String[] args) {
        testMethod();
    }

    synchronized public static void testMethod() {

    }
}

首先使用 javac Run.java 生成 class 文件

然后使用 javap -c -v Run.class 生成字节码:

核心字节码如下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #7                  // Method testMethod:()V
         3: return
      LineNumberTable:
        line 5: 0
        line 6: 3

  public static synchronized void testMethod();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 10: 0

在字节码指令中,对于 synchronized public static void testMethod() 使用了 flag 标记 ACC_SYNCHRONIZED ,说明此方法时同步的。

如果使用 synchronized 标记代码块而不是方法,则使用 monitorentermonitorexit 进行同步处理,例子如下:

public class Run {
    public static void main(String[] args) {
        Run run = new Run();
        run.testMethod();
    }

    public void testMethod() {
        synchronized (this) {
            int age = 100;
        }
    }
}

使用上述同样的方法查看字节码指令:

 public void testMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: bipush        100
         6: istore_2
         7: aload_1
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_1
        14: monitorexit
        15: aload_3
        16: athrow
        17: return

1.4 多个对象多个锁

例子:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        PrivateNum privateNumA = new PrivateNum();
        PrivateNum privateNumB = new PrivateNum();

        ThreadA threadA = new ThreadA(privateNumA);
        threadA.start();
        ThreadB threadB = new ThreadB(privateNumB);
        threadB.start();
    }
}

class PrivateNum {
    private int num = 0;

    synchronized public void addI(String name) {
        try {
            if (name.equals("a")) {
                num = 100;
                System.out.println("set a over");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("set b over");
            }
            System.out.println("num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread {

    private PrivateNum privateNum;

    public ThreadA(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("a");
    }
}

class ThreadB extends Thread {

    private PrivateNum privateNum;

    public ThreadB(PrivateNum privateNum) {
        this.privateNum = privateNum;
    }

    @Override
    public void run() {
        super.run();
        privateNum.addI("b");
    }
}

输出:

set a over
set b over
num=200
num=100

由输出可知,两个线程是以异步的方式进行

在例子中,创建了两个业务对象,在系统中产生了两个锁,线程和业务属于一对一的关系,每个线程都执行自己所属的业务对象的同步方法,而不是执行同一个业务对象,所以不存在争抢关系,所以是异步运行的。

关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或者方法当做锁,所以在例子中,哪个线程先执行 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁 Lock,其他线程都是处于等待状态,前提是多个线程访问的是同一个对象。如果多个线程访问多个对象,则 JVM 会创建多个锁。

1.5 将 synchronized 方法与对象作为锁

例子:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        ThreadA threadA = new ThreadA(object);
        threadA.setName("a");
        ThreadB threadB = new ThreadB(object);
        threadB.setName("b");

        threadA.start();
        threadB.start();
    }
}

class ThreadA extends Thread {

    private MyObject object;

    public ThreadA(MyObject object) {
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

class ThreadB extends Thread {

    private MyObject object;

    public ThreadB(MyObject object) {
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

class MyObject {
    public void methodA() {
        try {
            System.out.println("begin methodA thread name=" + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

begin methodA thread name=b
begin methodA thread name=a
end
end

通过观察输出可以看到两个线程可以一同进入 methodA() 方法

public void methodA() 方法添加 synchronized 关键字,重新执行:

begin methodA thread name=a
end
begin methodA thread name=b
end

通过输出可以发现,调用 synchronized 声明的方法一定是排队进行运行的。只有共享资源的读写访问才需要同步化,如果不是共享资源,则没有同步的必要性

其他方法在被调用时会有什么效果呢?

例子如下,新增方法:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        ThreadA threadA = new ThreadA(object);
        threadA.setName("a");
        ThreadB threadB = new ThreadB(object);
        threadB.setName("b");

        threadA.start();
        threadB.start();
    }
}

class ThreadA extends Thread {

    private MyObject object;

    public ThreadA(MyObject object) {
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

class ThreadB extends Thread {

    private MyObject object;

    public ThreadB(MyObject object) {
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodB();
    }
}

class MyObject {
    synchronized public void methodA() {
        try {
            System.out.println("begin methodA thread name=" + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("end endtime" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodB() {
        try {
            System.out.println("begin methodB thread name=" + Thread.currentThread().getName() + " begin time "
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

begin methodA thread name=a
begin methodB thread name=b begin time 1638365776489
end
end endtime1638365781491

通过例子可以得知,虽然线程A持有了object对象的锁,但是线程B可以异步得调用非 synchronized 的方法

如果方法B添加了 synchronized 关键字,则效果如下:

begin methodA thread name=a
end endtime1638365980157
begin methodB thread name=b begin time 1638365980157
end

通过上述例子可以得出:

  • A线程持有 object 对象的 Lock 锁,B线程可以异步调用 object 对象中的非 synchronized 类型的方法
  • A线程持有 object 对象的 Lock 锁,B线程如果调用 object 对象中的 synchronized 方法,则需要等待,也就是同步
  • 在方法处添加 synchronized 并不是锁方法,是锁当前类的对象
  • 在 Java 中,只有“将对象作为锁”,没有“锁方法”
  • 在 Java 中,“锁”就是“对象”,“对象”可以映射成“锁”,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的 synchronized 方法
  • 如果在 X 对象中使用了 synchronized 关键字声明非静态方法,则 X 对象就被当成锁
1.6 synchronized 锁重入

“可重入锁”是指自己可以再次获取自己的内部锁。例如,一个线程获得了某个对象锁,此时这个对象锁还没有被释放,当其再次想要获取这个对象锁的时候,还是能够获取到的。

例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

class Service {
    synchronized public void service1() {
        System.out.println("service1");
        service2();
    }

    synchronized public void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized public void service3() {
        System.out.println("service3");
    }
}

class MyThread extends Thread {

    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }

}

输出:

service1
service2
service3

由此可见,使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时可以得到该对象锁,这也证明在 synchronized 方法/块的内部调用其他 synchronized 方法/块时,是永远可以得到锁的。

1.7 锁重入支持继承的环境

锁重入也支持父子类继承的场景,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

class Base {
    public int i = 10;

    synchronized public void operateIInBaseMethod() {
        try {
            i--;
            System.out.println("base print i=" + i);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Sub extends Base {
    synchronized public void operateIInSubMethod() {
        try {
            while (i > 0) {
                i--;
                System.out.println("sub print i=" + i);
                Thread.sleep(100);
                super.operateIInBaseMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThread extends Thread{

    @Override
    public void run() {
        Sub sub=new Sub();
        sub.operateIInSubMethod();
    }
    
}

输出:

sub print i=9
base print i=8
sub print i=7
base print i=6
sub print i=5
base print i=4
sub print i=3
base print i=2
sub print i=1
base print i=0

1.8 出现异常,锁自动释放

当一个线程出现异常时,其多持有的锁会自动释放

1.9 重写方法不适用 synchronized

重现方法如果不使用 synchronized 关键字,即是非同步方法,使用后变成同步方法

例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Sub sub = new Sub();
        MyThreadA threadA = new MyThreadA(sub);
        threadA.setName("a");
        threadA.start();

        MyThreadB threadB = new MyThreadB(sub);
        threadB.setName("b");
        threadB.start();
    }
}

class Base {
    public int i = 10;

    synchronized public void serviceMethod() {
        try {
            System.out.println("in Base,sleep begin,thread Name" + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("in Base,sleep end,thread Name" + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Sub extends Base {

    @Override
    public void serviceMethod() {
        try {
            System.out.println("in sub,sleep begin,thread Name" + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("in sub,sleep end,thread Name" + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            super.serviceMethod();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class MyThreadA extends Thread {

    private Sub sub;

    public MyThreadA(Sub sub) {
        super();
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }

}

class MyThreadB extends Thread {

    private Sub sub;

    public MyThreadB(Sub sub) {
        super();
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }

}

输出:

in sub,sleep begin,thread Namea time=1638369761495
in sub,sleep begin,thread Nameb time=1638369761495
in sub,sleep end,thread Namea time=1638369766496
in sub,sleep end,thread Nameb time=1638369766496
in Base,sleep begin,thread Namea time=1638369766496
in Base,sleep end,thread Namea time=1638369771500
in Base,sleep begin,thread Nameb time=1638369771500
in Base,sleep end,thread Nameb time=1638369776501

从输出结果看,线程以异步的方式进行输出,如果在子类的重写方法中添加 synchronized 关键字,输出如下:

in sub,sleep begin,thread Namea time=1638369886262
in sub,sleep end,thread Namea time=1638369891264
in Base,sleep begin,thread Namea time=1638369891264
in Base,sleep end,thread Namea time=1638369896266
in sub,sleep begin,thread Nameb time=1638369896266
in sub,sleep end,thread Nameb time=1638369901267
in Base,sleep begin,thread Nameb time=1638369901267
in Base,sleep end,thread Nameb time=1638369906268

1.10 public static native boolean holdsLock(Object obj) 的使用

作用是当 currentThread 在指定对象上保持锁定时,才会返回 true

例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("A " + Thread.currentThread().holdsLock(Run.class));
        synchronized (Run.class) {
            System.out.println("B " + Thread.currentThread().holdsLock(Run.class));

        }
        System.out.println("C " + Thread.currentThread().holdsLock(Run.class));

    }
}

输出:

A false
B true
C false

2、 synchronized 同步语句块

与同步方法的差异:

  • synchronized 方法是将当前对象作为锁
  • synchronized 代码块是将任意对象作为锁

可以将锁看出一个标识,哪个线程持有这个标识,就可以执行同步方法

2.1 synchronized 同步方法的弊端

例子:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        MyThread1 thread1 = new MyThread1(task);
        thread1.start();
        MyThread2 thread2 = new MyThread2(task);
        thread2.start();
        Thread.sleep(10000);
        long beginTime = Math.min(CommonUtils.beginTime1, CommonUtils.beginTime2);
        long endTime = Math.max(CommonUtils.endTime1, CommonUtils.endTime2);
        System.out.println("time:" + (endTime - beginTime) / 1000);
    }
}

class Task {
    private String getData1;
    private String getData2;

    public synchronized void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            getData1 = "long time task result1 threadName=" + Thread.currentThread().getName();
            getData2 = "long time task result2 threadName=" + Thread.currentThread().getName();
            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class CommonUtils {
    public static long beginTime1;
    public static long endTime1;
    public static long beginTime2;
    public static long endTime2;
}

class MyThread1 extends Thread {
    private Task task;

    public MyThread1(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();

    }

}

class MyThread2 extends Thread {
    private Task task;

    public MyThread2(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();

    }

}

输出:

begin task
long time task result1 threadName=Thread-0
long time task result2 threadName=Thread-0
end task
begin task
long time task result1 threadName=Thread-1
long time task result2 threadName=Thread-1
end task
time:6

由例子和输出可以看出,在执行耗时任务时,两个线程依次进入耗时函数,在停止3s等待返回值处串行,导致效率不够高

2.2 使用同步代码块解决上述弊端

修改例子:

...
class Task {
    private String getData1;
    private String getData2;

    public void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            // 使用synchronized同步代码块
            synchronized (this) {
                getData1 = "long time task result1 threadName=" + Thread.currentThread().getName();
                getData2 = "long time task result2 threadName=" + Thread.currentThread().getName();
                System.out.println(getData1);
                System.out.println(getData2);
                System.out.println("end task");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
...

输出:

begin task
begin task
long time task result1 threadName=Thread-0
long time task result2 threadName=Thread-0
end task
long time task result1 threadName=Thread-1
long time task result2 threadName=Thread-1
end task
time:3

通过上述例子与输出可知,当一个线程访问 object 的一个 synchronized 同步代码块时,另一个线程仍然可以访问该 object 对象中的非 synchronized(this) 代码块

2.3 一半异步,一半同步

下列例子说明了不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        MyThread thread1 = new MyThread(task);
        MyThread thread2 = new MyThread(task);
        thread1.start();
        thread2.start();
    }
}

class Task {

    public void doLongTimeTask() {
        for (int i = 0; i < 10; i++) {
            System.out.println("no synchronized threadName=" + Thread.currentThread().getName() + " i=" + i);
        }
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println("synchronized threadName=" + Thread.currentThread().getName() + " i=" + i);
            }
        }
    }
}

class MyThread extends Thread {
    private Task task;

    public MyThread(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        task.doLongTimeTask();
    }

}

输出:

...
no synchronized threadName=Thread-0 i=5
no synchronized threadName=Thread-0 i=6
no synchronized threadName=Thread-0 i=7
no synchronized threadName=Thread-1 i=7
no synchronized threadName=Thread-0 i=8
no synchronized threadName=Thread-0 i=9
no synchronized threadName=Thread-1 i=8
no synchronized threadName=Thread-1 i=9
synchronized threadName=Thread-0 i=0
synchronized threadName=Thread-0 i=1
synchronized threadName=Thread-0 i=2
synchronized threadName=Thread-0 i=3
synchronized threadName=Thread-0 i=4
synchronized threadName=Thread-0 i=5
synchronized threadName=Thread-0 i=6
synchronized threadName=Thread-0 i=7
synchronized threadName=Thread-0 i=8
synchronized threadName=Thread-0 i=9
synchronized threadName=Thread-1 i=0
synchronized threadName=Thread-1 i=1
synchronized threadName=Thread-1 i=2
...

2.4 synchronized 代码块间的同步性

在使用 synchronized(this) 同步代码块时需要注意,当一个线程访问 object 的一个 synchronized(this) 同步代码块时,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码的访问将被阻塞,这说明 synchronized 使用的对象监视器是同一个,即使用的锁是同一个,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        ThreadA thread1 = new ThreadA(task);
        thread1.setName("a");
        ThreadB thread2 = new ThreadB(task);
        thread2.setName("b");
        thread1.start();
        thread2.start();
    }
}

class Task {

    public void taskMethod1() {
        try {
            synchronized (this) {
                System.out.println("A begin time=" + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time=" + System.currentTimeMillis());

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void taskMethod2() {
        synchronized (this) {
            System.out.println("B begin time=" + System.currentTimeMillis());
            System.out.println("B end time=" + System.currentTimeMillis());
        }
    }
}

class ThreadA extends Thread {
    private Task task;

    public ThreadA(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        task.taskMethod1();
    }

}

class ThreadB extends Thread {
    private Task task;

    public ThreadB(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        task.taskMethod2();
    }

}

输出:

A begin time=1639207075267
A end time=1639207077268
B begin time=1639207077268
B end time=1639207077268

2.5 println() 方法也是同步的

查看 println() 的源码可以发现也是运用 synchronized(this) 同步代码块,这样在输出信息的过程中是同步的,不会出现输出信息交叉导致信息混乱的情况

public void println(char[] x) {
    if (getClass() == PrintStream.class) {
        writeln(x);
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}


public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
2.6 验证 synchronized(this) 同步代码块是锁定当前对象的

synchronized 同步方法一样,synchronized(this)也是锁定当前对象的,例子:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        ThreadA thread1 = new ThreadA(task);
        thread1.start();
        Thread.sleep(10);
        ThreadB thread2 = new ThreadB(task);
        thread2.start();
    }
}

class Task {

    public void otherMethod() {
        System.out.println("----run other method----");
    }

    public void doLongTimeTask() {
        synchronized (this) {
            for (int i = 0; i < 500; i++) {
                System.out.println("synchronized threadName=" + Thread.currentThread().getName() + " i=" + i);
            }
        }
    }
}

class ThreadA extends Thread {
    private Task task;

    public ThreadA(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        task.doLongTimeTask();
    }

}

class ThreadB extends Thread {
    private Task task;

    public ThreadB(Task task) {
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        task.otherMethod();
    }

}

输出:

...
synchronized threadName=Thread-0 i=68
synchronized threadName=Thread-0 i=69
synchronized threadName=Thread-0 i=70
synchronized threadName=Thread-0 i=71
----run other method----
synchronized threadName=Thread-0 i=72
synchronized threadName=Thread-0 i=73
...

otherMethod() 增加 synchronized 关键字,输出如下:

...
synchronized threadName=Thread-0 i=496
synchronized threadName=Thread-0 i=497
synchronized threadName=Thread-0 i=498
synchronized threadName=Thread-0 i=499
----run other method----

由此可见,同步输出的原因是 synchronized (this) 同步代码块将当前类的对象作为锁,使用 public synchronized void otherMethod() 同步方法是当前方法所在的类的对象作为锁,都是同一把锁,所以运行结果表现出同步的效果

2.7 将任意对象作为锁

多个线程调用同一个对象中的不同名称的 synchronized 同步方法或者 synchronized(this) 同步代码块时,调用顺序是同步,即顺序执行。

synchronized 同步方法或者 synchronized(this) 同步代码块分别有两个作用

synchronized 同步方法的作用:

  • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块的调用呈现同步效果
  • 同一个时间只有同一个线程可以执行 synchronized 同步方法的代码

synchronized(this) 同步代码块的作用:

  • 对其他 synchronized 同步方法或 synchronized(this) 同步代码块的调用呈现同步效果
  • 同一个时间只有同一个线程可以执行 synchronized(this) 同步方法的代码

除了使用 synchronized(this) 的格式来创建同步代码块,其实 Java 还支持将“任意对象”作为锁来实现同步功能,这个”任意对象“大多数是实例变量及方法参数。使用格式为 synchronized(非this对象)

synchronized(非this对象) 同步代码块的作用:当多个线程争抢相同的“非this对象”的锁时。同一时间只有一个线程可以执行 synchronized(非this对象) 同步代码块中的代码,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        ThreadA thread1 = new ThreadA(service);
        ThreadB thread2 = new ThreadB(service);
        thread1.setName("A");
        thread2.setName("B");
        thread1.start();
        thread2.start();
    }
}

class Service {
    private String user;
    private String password;
    private String anyString = new String();

    public void setUserAndPassword(String user, String password) {
        try {
            synchronized (anyString) {
                System.out.println(
                        "Thread Name:" + Thread.currentThread().getName() + " at time:" + System.currentTimeMillis()
                                + " enter code");
                this.user = user;
                Thread.sleep(3000);
                this.password = password;
                System.out.println(
                        "Thread Name:" + Thread.currentThread().getName() + " at time:" + System.currentTimeMillis()
                                + " exit code");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.setUserAndPassword("a", "aa");
    }

}

class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.setUserAndPassword("b", "bb");
    }

}

输出:

Thread Name:A at time:1639210081549 enter code
Thread Name:A at time:1639210084551 exit code
Thread Name:B at time:1639210084551 enter code
Thread Name:B at time:1639210087552 exit code

锁非this对象的优点: 如果一个类中有很多 synchronized 方法,虽然能实行同步,但是影响效率;如果使用同步代码块锁非this对象,则 synchronized(非this对象) 代码块中程序与其他的同步方法都是异步的,因为有两把锁,不与其他锁this同步方法争抢this锁,可以大大提高运行效率

2.8 多个锁就是异步运行

更改2.7中 Service 代码,如下:

class Service {
    private String user;
    private String password;

    public void setUserAndPassword(String user, String password) {
        try {
            // 锁不同的对象
            String anyString = new String();
            synchronized (anyString) {
                System.out.println(
                        "Thread Name:" + Thread.currentThread().getName() + " at time:" + System.currentTimeMillis()
                                + " enter code");
                this.user = user;
                Thread.sleep(3000);
                this.password = password;
                System.out.println(
                        "Thread Name:" + Thread.currentThread().getName() + " at time:" + System.currentTimeMillis()
                                + " exit code");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出如下:

Thread Name:B at time:1639210613260 enter code
Thread Name:A at time:1639210613262 enter code
Thread Name:B at time:1639210616262 exit code
Thread Name:A at time:1639210616264 exit code

可见,想使用 synchronized(非this对象) 同步代码块时,锁必须是同一个,否则就是异步运行

2.9 细化三个结论

synchronized(非this对象x) 的写法是将 x 对象本身作为“对象监视器”,这样就可以分析出3个结论:

  • 当多线程同时执行 synchronized(x){} 同步代码块时呈现同步效果
  • 当其他线程执行 x 对象中的 synchronized 同步方法时呈现同步效果
  • 当其他线程执行 x 对象中的 synchronized(this) 代码块时呈现同步效果

需要注意,如果其他线程不加 synchronized 关键字的方法,则还是会异步调用

验证第一个结论,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        MyObject object = new MyObject();
        ThreadA thread1 = new ThreadA(service, object);
        ThreadA thread2 = new ThreadA(service, object);
        thread1.setName("a");
        thread2.setName("b");
        thread1.start();
        thread2.start();
    }
}

class MyObject {

}

class Service {

    public void testMethod1(MyObject object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1 get lock time=" + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
                Thread.sleep(2000);
                System.out.println("testMethod1 release lock time=" + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

class ThreadA extends Thread {
    private Service service;
    private MyObject object;

    public ThreadA(Service service, MyObject object) {
        this.service = service;
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }

}

输出:

testMethod1 get lock time=1639212493316 run ThreadName=a
testMethod1 release lock time=1639212495317 run ThreadName=a
testMethod1 get lock time=1639212495317 run ThreadName=b
testMethod1 release lock time=1639212497320 run ThreadName=b

同步的原因是使用了同一个锁,如果使用不同的锁会怎么样呢?修改 main 函数,如下:

public static void main(String[] args) throws InterruptedException {
    Service service = new Service();
    MyObject object1 = new MyObject();
    MyObject object2 = new MyObject();
    ThreadA thread1 = new ThreadA(service, object1);
    ThreadA thread2 = new ThreadA(service, object2);
    thread1.setName("a");
    thread2.setName("b");
    thread1.start();
    thread2.start();
}

输出:

testMethod1 get lock time=1639212646169 run ThreadName=b
testMethod1 get lock time=1639212646169 run ThreadName=a
testMethod1 release lock time=1639212648170 run ThreadName=a
testMethod1 release lock time=1639212648170 run ThreadName=b

验证第二个结论,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        MyObject object = new MyObject();
        ThreadA thread1 = new ThreadA(service, object);
        ThreadB thread2 = new ThreadB(service, object);
        thread1.setName("a");
        thread2.setName("b");
        thread1.start();
        thread2.start();
    }
}

class MyObject {
    public synchronized void speedPrintString() {
        System.out.println("speedPrintString get lock time=" + System.currentTimeMillis() + " run ThreadName="
                + Thread.currentThread().getName());
        System.out.println("----------");
        System.out.println("speedPrintString release lock time=" + System.currentTimeMillis() + " run ThreadName="
                + Thread.currentThread().getName());

    }
}

class Service {

    public void testMethod1(MyObject object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1 get lock time=" + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
                Thread.sleep(5000);
                System.out.println("testMethod1 release lock time=" + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

class ThreadA extends Thread {
    private Service service;
    private MyObject object;

    public ThreadA(Service service, MyObject object) {
        this.service = service;
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }

}

class ThreadB extends Thread {
    private Service service;
    private MyObject object;

    public ThreadB(Service service, MyObject object) {
        this.service = service;
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.speedPrintString();
    }

}

输出:

testMethod1 get lock time=1639213210778 run ThreadName=a
testMethod1 release lock time=1639213215781 run ThreadName=a
speedPrintString get lock time=1639213215781 run ThreadName=b
<---------->
speedPrintString release lock time=1639213215781 run ThreadName=b

验证第三个结论,将第二个例子中的 public void speedPrintString() 略作修改:

class MyObject {
    public void speedPrintString() {
        synchronized (this) {
            System.out.println("speedPrintString get lock time=" + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
            System.out.println("<---------->");
            System.out.println("speedPrintString release lock time=" + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
        }

    }
}

输出:

speedPrintString get lock time=1639213337837 run ThreadName=b
<---------->
speedPrintString release lock time=1639213337837 run ThreadName=b
testMethod1 get lock time=1639213337837 run ThreadName=a
testMethod1 release lock time=1639213342839 run ThreadName=a

2.10 类 Class 的单例性

每一个 *.java 文件对应的 Class 类的实例都是一个,在内存中都是单例的,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyClass class1 = new MyClass();
        MyClass class2 = new MyClass();
        MyClass class3 = new MyClass();
        System.out.println(class1.getClass() == class1.getClass());
        System.out.println(class1.getClass() == class2.getClass());
        System.out.println(class1.getClass() == class3.getClass());
    }
}

class MyClass {

}

输出:

true
true
true

Class 类用于描述类的基本信息,包括多少个字段,有多少个构造方法,有多少个普通方法,为了减少对内存的高占用率,在内存中只需要存在一份 Class 类对象就可以了,所以被设计成单例的

2.11 静态同步 synchronized 方法与 synchronized(class) 代码块

关键字 synchronized 还可以应用在 static 静态方法上,如果这样声明,*则是对 .java 对应的 Class 类对象进行加锁,Class 对象是单例的。更具体的说,在静态 static 方法上使用 synchronized 关键字声明同步方法,则是使用当前静态方法所在对应 Class 类的单例对象作为锁。例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        ThreadA a = new ThreadA();
        ThreadB b = new ThreadB();
        a.setName("a");
        b.setName("b");
        a.start();
        b.start();
    }
}

class Service {
    synchronized public static void printA() {
        try {
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " enter printA");
            Thread.sleep(3000);
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " exit printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public static void printB() {
        System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                + " enter printB");
        System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                + " exit printB");
    }
}

class ThreadA extends Thread {

    @Override
    public void run() {
        Service.printA();
    }

}

class ThreadB extends Thread {

    @Override
    public void run() {
        Service.printB();
    }

}

输出:

Thread name:a at 1639569863166 enter printA
Thread name:a at 1639569866168 exit printA
Thread name:b at 1639569866168 enter printB
Thread name:b at 1639569866168 exit printB

虽然该代码段的运行结果和将 synchronized 关键字加到非 static 方法上的效果是一样的,但是本质有所不同。synchronized 关键字加到static 静态方法是将 Class 的对象作为锁,而 synchronized 加到非 static 静态方法是将类的对象作为锁。

2.12 同步 synchronized static 方法可以对类的所有实例对象起作用

Class 锁可以对类的所有对象实例起作用,例子如下:

public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service serviceA = new Service();
        Service serviceB = new Service();
        ThreadA a = new ThreadA(serviceA);
        ThreadB b = new ThreadB(serviceB);
        a.setName("a");
        b.setName("b");
        a.start();
        b.start();
    }
}

class Service {
    synchronized public static void printA() {
        try {
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " enter printA");
            Thread.sleep(3000);
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " exit printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public static void printB() {
        System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                + " enter printB");
        System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                + " exit printB");
    }
}

class ThreadA extends Thread {

    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.printA();
    }

}

class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        service.printB();
    }

}

输出如下:

Thread name:a at 1639570587052 enter printA
Thread name:a at 1639570590053 exit printA
Thread name:b at 1639570590053 enter printB
Thread name:b at 1639570590053 exit printB

2.12 同步 synchronized(class) 代码块可以对类的所有实例对象起作用

同步 synchronized(class) 代码块的作用其实和 synchronized static 方法的作用一样,将前一节的Serive稍作修改:

class Service {
    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out
                        .println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                                + " enter printA");
                Thread.sleep(3000);
                System.out
                        .println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                                + " exit printA");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void printB() {
        synchronized (Service.class) {
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " enter printB");
            System.out.println("Thread name:" + Thread.currentThread().getName() + " at " + System.currentTimeMillis()
                    + " exit printB");
        }

    }
}

输出如下:

Thread name:a at 1639570824310 enter printA
Thread name:a at 1639570827310 exit printA
Thread name:b at 1639570827310 enter printB
Thread name:b at 1639570827310 exit printB