5. synchronized关键字(二)

一、synchronized同步代码块

使用关键字 synchronized 声明的方法在某些情况下是有弊端的,比如线程 A 调用同步方法执行一个长时间的任务,那么线程 B 必须等待线程 A 将这个同步方法全部执行完才可以调用该方法,但是很多时候我们不需要对整个方法进行同步,可能只需要对部分代码块进行同步,这个时候就用到了synchronized同步代码块

直接看个使用 synchronized 同步代码块的例子吧

//创建两个线程一起调用的类 TaskA
class TaskA {
	
    //没有使用 synchronized(this) 代码块
    public void doLongTimeTask() {
        for (int i = 0; i < 50; i++) {
            System.out.println("no synchronized " + Thread.currentThread().getName()
                    + " i = " + (i + 1));
        }

        System.out.println("");

        //使用了 synchronized(this) 代码块
        synchronized (this) {
            for (int i = 0; i < 50; i++) {
                System.out.println("synchronized " + Thread.currentThread().getName()
                        + " i = " + (i + 1));
            }
        }
    }

}

//创建线程 ThreadB1
class ThreadB1 extends Thread {

    private TaskA taskA;

    public ThreadB1(TaskA taskA) {
        this.taskA = taskA;
    }

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

}

//创建线程 ThreadA1
public class ThreadA1 extends Thread {

    private TaskA taskA;

    public ThreadA1(TaskA taskA) {
        this.taskA = taskA;
    }

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

    public static void main(String[] args) {
        TaskA task = new TaskA();
        ThreadA1 threadA1 = new ThreadA1(task);
        threadA1.setName("AAA");
        threadA1.start();

        ThreadB1 threadB1 = new ThreadB1(task);
        threadB1.setName("BBB");
        threadB1.start();
    }
}

我们截取了三种类型的结果来看:

//两个线程交替进行的结果,只是部分
no synchronized AAA i = 1
no synchronized BBB i = 1
no synchronized AAA i = 2
no synchronized BBB i = 2
no synchronized BBB i = 3
no synchronized AAA i = 3
no synchronized BBB i = 4
no synchronized BBB i = 5
no synchronized AAA i = 4
no synchronized AAA i = 5
no synchronized AAA i = 6
no synchronized AAA i = 7
no synchronized BBB i = 6
no synchronized AAA i = 8
no synchronized BBB i = 7
no synchronized BBB i = 8
no synchronized AAA i = 9
no synchronized BBB i = 9
no synchronized AAA i = 10
...
//两个线程各自同步执行的结果
synchronized AAA i = 45
synchronized AAA i = 46
synchronized AAA i = 47
synchronized AAA i = 48
synchronized AAA i = 49
synchronized AAA i = 50
synchronized BBB i = 1
synchronized BBB i = 2
synchronized BBB i = 3
synchronized BBB i = 4
synchronized BBB i = 5
synchronized BBB i = 6
synchronized BBB i = 7
...
//一个同步一个异步执行的情况
synchronized AAA i = 10
synchronized AAA i = 11
synchronized AAA i = 12
synchronized AAA i = 13
synchronized AAA i = 14
synchronized AAA i = 15
no synchronized BBB i = 42
synchronized AAA i = 16
no synchronized BBB i = 43
synchronized AAA i = 17
no synchronized BBB i = 44
synchronized AAA i = 18
no synchronized BBB i = 45

对于第一种,线程 A 和线程 B 同时访问到了方法 synchronized 块中的部分,这个时候,可以随意执行,随意交替
对于第二种,线程 A 和线程 B 同时访问到了方法不在 synchronized 块中的部分,这个时候,如果线程 A 先进入 synchronized 代码块,那么线程 B 必须等待线程 A 执行完才可以进入
对于第三种,线程 A 访问到了方法在 synchronized 块中的部分,线程 B 则访问到了方法不在 synchronized 块中的部分,此时尽管线程 A 是同步执行的,但是线程 B 是异步执行的,这个时候,线程 B 就是随意执行了

简单的说:不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行

二、synchronized代码块的同步性

当一个线程访问 object 的一个 synchronized(this) 同步代码块时,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码块的访问都将被阻塞

//创建一个公共类
class ObjectService2 {

    public void serviceMethodA() {
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName()
                        + " methodA begin Time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()
                        + " methodA end Time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

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

创建两个线程对同一个对象进行操作

//创建线程 ThreadB2
class ThreadB2 extends Thread {

    private ObjectService2 service2;

    public ThreadB2(ObjectService2 service2) {
        this.service2 = service2;
    }

    @Override
    public void run() {
        //线程 BBB 访问 ObjectService2 中的 synchronized(this) 同步代码块
        service2.serviceMethodB();
    }

}

//创建线程 ThreadA2
public class ThreadA2 extends Thread {

   private ObjectService2 service2;

    public ThreadA2(ObjectService2 service2) {
        this.service2 = service2;
    }

    @Override
    public void run() {
        //线程 AAA 访问 ObjectService2 中的 synchronized(this) 同步代码块
        service2.serviceMethodA();
    }

    public static void main(String[] args) {
        ObjectService2 service2 = new ObjectService2();
        ThreadA2 threadA2 = new ThreadA2(service2);
        threadA2.setName("AAA");
        threadA2.start();

        ThreadB2 threadB2 = new ThreadB2(service2);
        threadB2.setName("BBB");
        threadB2.start();
    }
}

结果是:

AAA methodA begin Time = 1540186290804
AAA methodA end Time = 1540186292804
BBB methodB begin time = 1540186292804
BBB methodB end time = 1540186294805

从结果看到,两个线程调用的方法都是同步执行的,线程 B 等待线程 A 执行完 synchronized 代码块之后再执行自己的 synchronized 代码块。这是因为两个线程调用都是对同一个对象(service2)进行操作,即线程 A 先获得对象锁,线程 B 只有等到线程 A 执行完并且释放锁才能执行自己的代码块

三、synchronized代码块和synchronized方法

如果一个类中,既有 synchronized 的代码块,又有 synchronized 修饰的方法,那么结果会是如何?

class Task3 {
	//synchronized 方法
    synchronized public void otherMethod() {
        System.out.println("--------------------------run--otherMethod");
    }

    //synchronized 代码块
    public void doLongTimeTask() {
        synchronized (this) {
            for (int i = 0; i < 50; i++) {
                System.out.println("synchronized "
                        + Thread.currentThread().getName() + " i = " + (i + 1));
            }
        }
    }

}
class ThreadB3 extends Thread {

    private Task3 task3;

    public ThreadB3(Task3 task3) {
        this.task3 = task3;
    }

    @Override
    public void run() {
        //线程 BBB 调用同步方法
        task3.otherMethod();
    }
}

public class ThreadA3 extends Thread {

    private Task3 task3;

    public ThreadA3(Task3 task3) {
        this.task3 = task3;
    }

    @Override
    public void run() {
        //线程 AAA 调用同步代码块
        task3.doLongTimeTask();
    }

    public static void main(String[] args) {
        Task3 task3 = new Task3();
        ThreadA3 threadA3 = new ThreadA3(task3);
        threadA3.setName("AAA");
        threadA3.start();

        ThreadB3 threadB3 = new ThreadB3(task3);
        threadB3.setName("BBB");
        threadB3.start();
    }
}

结果是:

...
synchronized AAA i = 40
synchronized AAA i = 41
synchronized AAA i = 42
synchronized AAA i = 43
synchronized AAA i = 44
synchronized AAA i = 45
synchronized AAA i = 46
synchronized AAA i = 47
synchronized AAA i = 48
synchronized AAA i = 49
synchronized AAA i = 50
--------------------------run--otherMethod

从结果看,synchronized(this) 代码块和 synchronized 方法都是同步执行的。这是因为,两个线程都是对同一个对象 task3 执行操作的,线程 A 先访问 synchronized(this) 代码块,即获取了对象 task3 的对象锁,这个时候,类中的另一个 synchronized 方法就被锁定了,线程 B 必须等线程 A 执行完那段代码才可以执行

为了验证这个例子 synchronized(this) 代码块和 synchronized 方法是对同一对象进行操作,我们打印出 this,结果是:

doLongTimeTask:edu.just.syn.Task3@1e542a06
otherMethod:edu.just.syn.Task3@1e542a06

两种方式都是对 this 进行操作,synchronized(this)代码块也是锁定当前对象的

四、将任意对象作为监视器

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

1.synchronized 同步方法

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

2.synchronized(this) 同步方法

  • 对其他 synchronized 同步方法或者 synchronize(this) 同步代码块调用呈阻塞状态
  • 同一时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码

其实 Java 还支持对“任意对象”作为“对象监视器”来实现同步的功能,这个“任意对象”大多数是实例变量或者方法的参数,使用的格式是 synchronized(非 this 对象)。看个例子吧

class Service {

    private String username;
    private String password;

    private Object anyObject = new Object();

    public void setUsernameAndPassword(String username, String password) {
//语句1       System.out.println(Thread.currentThread().getName() + " " + anyObject);
        
        //此时 anyString 对象作为对象监视器
        synchronized (anyObject) {
            try {
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                        + " 在 " + System.currentTimeMillis() + " 进入同步代码块 ");
                Thread.sleep(2000);
                this.password = password;
                this.username = username;
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                        + " 在 " + System.currentTimeMillis() + " 退出同步代码块");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

创建两个线程来调用

class ThreadB4 extends Thread {

    private Service service;

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

    @Override
    public void run() {
        service.setUsernameAndPassword("BBB","BBB123");
    }
}

public class ThreadA4 extends Thread {

    private Service service;

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

    @Override
    public void run() {
        service.setUsernameAndPassword("AAA", "AAA123");
    }

    public static void main(String[] args) {
        Service service = new Service();
        ThreadA4 threadA4 = new ThreadA4(service);
        threadA4.setName("线程 A");
        threadA4.start();

        ThreadB4 threadB4 = new ThreadB4(service);
        threadB4.setName("线程 B");
        threadB4.start();
    }
}

结果是:

线程名称为:线程 A 在 1540189492294 进入同步代码块
线程名称为:线程 A 在 1540189494295 退出同步代码块
线程名称为:线程 B 在 1540189494295 进入同步代码块
线程名称为:线程 B 在 1540189496296 退出同步代码块

从结果看出,使用 synchronized(anyObject) 的代码块,在“对象监视器”是同一对象的情况下,依然是同步执行的,先是线程 A 执行,然后执行完了之后才是线程 B 执行

怎么理解这里的同一对象呢?我们把语句1的注释拿掉,进行输出,只打印注释掉的语句输出的内容,结果如下:

线程 A java.lang.Object@322b4b46
线程 B java.lang.Object@322b4b46

emmm~~~看到了把,两个线程访问该方法时用到的是同一个对象,这是为啥呢?因为 private Object anyObject = new Object() 这个语句是在方法外面的,anyObject 对象是全局的,每次使用 synchronized(anyObject) 代码块进行同步操作时,对象监视器就是同一个对象 anyString,此时两个线程同步执行,即按照顺序执行

如果我们把 Object anyObject = new Object() 放在方法内部,此时 anObject是局部变量,然后输出结果,看看会怎样

线程 A java.lang.Object@45ffb1ad
线程 B java.lang.Object@20210b0
线程名称为:线程 A 在 1540190711705 进入同步代码块 1174385069
线程名称为:线程 B 在 1540190711706 进入同步代码块 33689776
线程名称为:线程 A 在 1540190713706 退出同步代码块
线程名称为:线程 B 在 1540190713707 退出同步代码块

哈哈,怎样,结果也猜到了把,这个时候两个线程访问的不同的变量,从输出的地址不同也能看出。如果 anyString 对象是在方法中的,局部的,那么每个线程调用这个方法都会 new 一个新的 anyString 对象,之后使用 synchronized(anyString) 同步代码块进行同步操作时,因为对象监视器不是同一个对象,此时运行的结果就是异步调用的,即两个线程交叉运行

总结一下:多个线程持有"对象监视器"为同一个对象的前提下,同一时间只能有一个线程可以执行synchronized(非this对象x)代码块中的代码

锁非 this 对象具有一定的优点:如果在一个类中有很多个 synchronized 方法,这是虽然能实现同步,但是会收到阻塞,即每个 synchronized 方法必须等待前一个 synchronized 方法执行完才能执行;但如果使用同步代码块锁非 this 对象,则 synchronized(非 this) 代码块中的程序与 synchronized 同步方法是异步的,它不与锁 this 同步方法争抢 this 锁,可以大大提高运行效率

我们可以再来看一个在 synchronized(非 this 对象) 中,“任意对象” 是方法参数的例子

class MyOneList {

    private List list = new ArrayList();

    synchronized public void addMethod(String data) {
        list.add(data);
    }

    synchronized public int getSize() {
        return list.size();
    }

}

//创建业务类,用于存放数据
class MyService {
    
    public MyOneList addServiceMethod(MyOneList myOneList, String data) {
        try {
            System.out.println(myOneList);

            synchronized (myOneList) {
                //如果如果集合长度小于 1
                if (myOneList.getSize() < 1) {
                    System.out.println(Thread.currentThread().getName() + " 进入了方法,时间是:" + System.currentTimeMillis());
                    //模拟从远程花费 2s 取回数据
                    Thread.sleep(2000);
                    //存入数据
                    myOneList.addMethod(data);

                    System.out.println(Thread.currentThread().getName() + " 存入了数据 " + data + " 时间是:" + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myOneList;
    }
    
}

创建两个对象

class ThreadB8 extends Thread {

    private MyOneList myOneList;

    public ThreadB8(MyOneList myOneList) {
        this.myOneList = myOneList;
    }

    @Override
    public void run() {
        MyService myService = new MyService();
        myService.addServiceMethod(myOneList, "BBB");
    }
}

public class ThreadA8 extends Thread {

    private MyOneList myOneList;

    public ThreadA8(MyOneList myOneList) {
        this.myOneList = myOneList;
    }

    @Override
    public void run() {
        MyService myService = new MyService();
        myService.addServiceMethod(myOneList, "AAA");
    }

    public static void main(String[] args) throws InterruptedException {
        MyOneList oneList = new MyOneList();
        ThreadA8 threadA8 = new ThreadA8(oneList);
        threadA8.setName("AAA");
        threadA8.start();

        ThreadB8 threadB8 = new ThreadB8(oneList);
        threadB8.setName("BBB");
        threadB8.start();

        Thread.sleep(4000);
        System.out.println("listSize " + oneList.getSize());
    }

}

结果是:

edu.just.syn.MyOneList@67c716ea
AAA 进入了方法,时间是:1540192561732
edu.just.syn.MyOneList@67c716ea
AAA 存入了数据 AAA 时间是:1540192563732
listSize 1

从结果看到,两个线程对方法 addServiceMethod 进行处理时,是对同一个对象 myOneList 执行操作的,此时该方法是同步方法,一次性只能在集合中存入一个值,不会出现脏读。本质上,只要两个线程在同步代码块中是对同一个对象进行处理的,那么就是该代码块就是同步执行的

五、参考

《Java多线程核心技术编程》
https://www.cnblogs.com/xrq730/p/4851530.html

猜你喜欢

转载自blog.csdn.net/babycan5/article/details/83278646