Java多线程编程笔记3:synchronized同步语句块

使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。

synchronized(this)同步代码块

当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块才能执行该代码块。

一个线程访问object的一个同步代码块时,另外一个线程仍然可以访问该对象的非同步代码块。 以下代码可以证明:不在synchronized块中就是异步执行,在synchronized块中就是同步执行。

class Task{
    public void doLongTimeTask(){
        for (int i=0;i<5;i++){
            System.out.println("nonsync threadName:"+Thread.currentThread().getName()+" i: "+i);
        }
        System.out.println();
        synchronized (this){
            for (int i=0;i<5;i++){
                System.out.println("sync threadName:"+Thread.currentThread().getName()+" i: "+i);
            }
        }
    }
}

class MyThread extends Thread{
    private Task task;
    public MyThread(Task task){
        this.task=task;
    }

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


public class Run {
    public static void main(String[] args) {
        Task task=new Task();
        MyThread t1=new MyThread(task);
        MyThread t2=new MyThread(task);
        t1.start();
        t2.start();
    }
}
复制代码

运行结果为:

nonsync threadName:Thread-0 i: 0
nonsync threadName:Thread-1 i: 0
nonsync threadName:Thread-0 i: 1
nonsync threadName:Thread-1 i: 1
nonsync threadName:Thread-1 i: 2
nonsync threadName:Thread-1 i: 3
nonsync threadName:Thread-0 i: 2
nonsync threadName:Thread-1 i: 4
nonsync threadName:Thread-0 i: 3

nonsync threadName:Thread-0 i: 4

sync threadName:Thread-1 i: 0
sync threadName:Thread-1 i: 1
sync threadName:Thread-1 i: 2
sync threadName:Thread-1 i: 3
sync threadName:Thread-1 i: 4
sync threadName:Thread-0 i: 0
sync threadName:Thread-0 i: 1
sync threadName:Thread-0 i: 2
sync threadName:Thread-0 i: 3
sync threadName:Thread-0 i: 4
复制代码

可以看到,非同步代码块是异步执行的,同步代码块则是排队执行的。

同步代码块之间的同步性:在使用synchronized(this)代码块时,当一个线程访问object的一个同步代码块时,其他线程对同一个对象中所有其他synchronized(this)代码块的访问会被阻塞,说明synchronized使用的“对象监视器是一个”。

synchronized(this)代码块是锁定当前对象的。也就是说,多个线程调用同一个对象的不同名称的synchronized同步方法或者synchronized(this)同步代码块时,调用的效果就是按顺序执行,就是同步的,阻塞的。也就是说,无论是同步方法或者synchronized(this)代码块,都可以对其他同步方法或代码块调用呈阻塞状态。同一时间只有一个线程可以执行同步方法或同步代码块中的代码。

synchronized(非this对象)代码块

除了synchronized(this)同步代码块,Java还支持对任意对象作为“对象监视器”来实现同步的功能。任意对象大多数是实例变量及方法的参数。同样的,在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this)同步代码块中的代码。

锁非this对象有一定的优点:如果在一个类中有很多synchronized方法,这时虽然能实现同步,但会收到阻塞,影响运行效率。但是如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以提高运行效率。

在使用synchronized(非this对象)同步代码块时,对象监视器必须是同一个对象,如果不是同一个对象监视器,运行的结果就是异步调用了。示例代码:

package ch02.t12;

class Service {
    private String anyStr = new String();

    public void method() {
        try {
            synchronized (anyStr) {
                System.out.println("Thread: " + Thread.currentThread().getName()
                        + " time: " + System.currentTimeMillis() + "进入同步块");
                Thread.sleep(3000);
                System.out.println("Thread: " + Thread.currentThread().getName()
                        + " time: " + System.currentTimeMillis() + "离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThread extends Thread{
    private Service service;
    public MyThread(Service service){
        this.service=service;
    }

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

public class Run {
    public static void main(String[] args) {
        Service service=new Service();
        MyThread a=new MyThread(service);
        a.setName("A");
        a.start();

        MyThread b=new MyThread(service);
        b.setName("B");
        b.start();
    }
}
复制代码

运行结果为:

Thread: A time: 1544337130842进入同步块
Thread: A time: 1544337133842离开同步块
Thread: B time: 1544337133842进入同步块
Thread: B time: 1544337136852离开同步块
复制代码

如果将Service改成:

class Service {
    public void method() {
        try {
            String anyStr = new String();
            synchronized (anyStr) {
                System.out.println("Thread: " + Thread.currentThread().getName()
                        + " time: " + System.currentTimeMillis() + "进入同步块");
                Thread.sleep(3000);
                System.out.println("Thread: " + Thread.currentThread().getName()
                        + " time: " + System.currentTimeMillis() + "离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

运行结果为:

Thread: B time: 1544337317933进入同步块
Thread: A time: 1544337317933进入同步块
Thread: A time: 1544337320933离开同步块
Thread: B time: 1544337320933离开同步块
复制代码

是异步进行的,因此此时两个anyStr对象不是同一个对象了。

synchronized代码块的“脏读”

同步代码块放在非同步方法中进行生命,并不能摆正调用方法的线程的执行同步、顺序性。线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样容易出现“脏读”问题。

class MyOneList{
    private ArrayList list=new ArrayList();
    synchronized public void add(String data){
        list.add(data);
    }
    synchronized public int getSize(){
        return list.size();
    }
}

class MyService{
    public MyOneList add(MyOneList list,String data){
        try {
            System.out.println(Thread.currentThread().getName());
            if(list.getSize()<1){//保证list只有一个元素
                Thread.sleep(2000);
                list.add(data);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return list;
    }
}

class MyThread extends Thread{
    private MyOneList list;
    public MyThread(MyOneList list){
        this.list=list;
    }

    @Override
    public void run() {
        MyService service=new MyService();
        service.add(list,"A");
    }
}

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyOneList list=new MyOneList();
        MyThread t1=new MyThread(list);
        t1.setName("A");
        t1.start();
        MyThread t2=new MyThread(list);
        t2.setName("B");
        t2.start();
        Thread.sleep(6000);
        System.out.println(list.getSize());

    }
}
复制代码

运行结果:

B
A
2
复制代码

首先可以发现,两个线程打印的顺序是无序的,说明线程的执行时异步的。返回的结果中list的size为2,原因是在add()方法中,list.getSize()这一方法是异步调用的。因此,需要对add()方法进行同步化,修改如下:

class MyService {
    public MyOneList add(MyOneList list, String data) {
        try {
            synchronized (list) {
                System.out.println(Thread.currentThread().getName());
                if (list.getSize() < 1) {//保证list只有一个元素
                    Thread.sleep(2000);
                    list.add(data);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return list;
    }
}
复制代码

list对象在项目中只有一份实例,且对其进行调用,因此以list参数进行同步处理。结果如下:

A
B
1
复制代码

synchronized(非this对象x)的三个结论

  1. 当多个线程同时执行synchronized(x)同步代码块时是同步的;
  2. 当线程A执行以x为锁的代码块时,其他线程执行x对象中的同步方法时也是同步的;
  3. 当线程A执行以x为锁的代码块时,其他线程执行x对象中的synchronized(this)代码块时也是同步的。

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

关键字synchronized还可以应用在static静态方法上,这样是堆当前的*.java文件对应的Class类加锁。

class Service{
    synchronized public static void printA(){
        try {
            System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printA方法");
            Thread.sleep(3000);
            System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printA方法");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public static void printB(){
            System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printB方法");
            System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printB方法");
    }

    synchronized public  void printC(){
        System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printC方法");
        System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printC方法");
    }
}

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();
    }
}


class ThreadC extends Thread{
    private Service service;
    public ThreadC(Service service){
        this.service=service;
    }
    @Override
    public void run() {
        service.printC();
    }
}

public class Run {
    public static void main(String[] args) {
        Service service=new Service();
        ThreadA a=new ThreadA(service);
        a.setName("A");
        a.start();
        ThreadB b=new ThreadB(service);
        b.setName("B");
        b.start();
        ThreadC c=new ThreadC(service);
        c.setName("C");
        c.start();
    }
}
复制代码

运行结果:

thread: A time: 1544341920925 进入printA方法
thread: C time: 1544341920925 进入printC方法
thread: C time: 1544341920925 离开printC方法
thread: A time: 1544341923925 离开printA方法
thread: B time: 1544341923925 进入printB方法
thread: B time: 1544341923925 离开printB方法
复制代码

可以看出,A与C并不是同步的,A与B是同步的。原因就是A和B是持有的同一个Class锁,而C持有的是对象锁。

但是,Class锁可以对类的所有对象实例起作用。修改上述的代码,只修改main方法如下:

public class Run {
    public static void main(String[] args) {
        Service service1=new Service();
        Service service2=new Service();
        ThreadA a=new ThreadA(service1);
        a.setName("A");
        a.start();
        ThreadB b=new ThreadB(service2);
        b.setName("B");
        b.start();
    }
}
复制代码

运行结果如下:

thread: A time: 1544342122668 进入printA方法
thread: A time: 1544342125668 离开printA方法
thread: B time: 1544342125668 进入printB方法
thread: B time: 1544342125668 离开printB方法
复制代码

可以看到,线程A和线程B虽然调用的是不同的对象,但是由于二者是Class锁,因此仍然是同步进行的。

synchronized(class)代码块和synchronized static方法的作用是一样的,修改上述的printA()和printB()方法如下:

class Service {
    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printA方法");
                Thread.sleep(3000);
                System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printA方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    synchronized public static void printB() {
        synchronized (Service.class) {
            System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printB方法");
            System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printB方法");
        }
    }
}
复制代码

Run类和刚刚修改后的两个不同的Service对象一样,运行结果为:

thread: A time: 1544342263970 进入printA方法
thread: A time: 1544342266970 离开printA方法
thread: B time: 1544342266970 进入printB方法
thread: B time: 1544342266970 离开printB方法
复制代码

可以看到效果是一样的。

synchronized(string)同步代码块

在JVM中,具有String常量池缓存功能。在将synchronized(string)使用时,需要注意常量池带来的一些例外。在大多数情况下,同步代码块不推荐使用String作为锁对象,而改用其他,比如new Object(),它不放入缓存中。

多线程的死锁

死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。

常用的解决方案如下:

  • 尽量一个线程只获取一个锁。
  • 一个线程只占用一个资源。
  • 尝试使用定时锁,至少能保证锁最终会被释放。
内置类与静态内置类

判断是否同步的方法是一样的,都是判断是否是同一个对象为锁。

锁对象的改变

如果锁对象本身改变了(如String从“a”变成了“b”),则改变前和改变后,线程的锁不一样,是异步的。但是如果锁的属性改变了,但是锁对象本身没变,则仍然是同步的。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值