java线程同步之Synchronized

前言:这段时间项目里发生了主线程anr的问题需要我解决,在一般的情况下anr都是主线程做了耗时的操作造成的。但是经过各种排查最后我发现我的这个问题是和线程的同步问题有关系的,对象的加锁引起了线程的阻塞。所以,借着这个事情我打算把线程同步的情况给总结一下,写出来给大家看看。

这篇文章和以往的风格不太一样,全部充斥的都是理论,所以希望你可以坚持的看完,相信自己,你可以的,fighting!!!

开篇就先给大家做一个锁的概念性的介绍吧,希望对面的你可以喜欢大笑


先对锁分类一下下:

一般按照名称来说,锁大概可以分为: 自旋锁 ,阻塞锁,可重入锁 ,读写锁 ,互斥锁 ,悲观锁 ,乐观锁 ,公平锁,偏向锁, 对象锁,线程锁,锁粗化,轻量级锁,重量级锁,独享锁,共享锁,分段锁 。


但是我们平时所说的锁的分类其实应该按照锁的特性和设计来划分,依据设计方案和性质已经对锁进行了大量的划分。

可是,可是,可是,对于我们开发来说,我们真正用到的也就是两三种,包括Synchronized和Lock


先简介一下Synchronized和Lock:

Synchronized是java原生语义上的实现:它就是一个 非公平,悲观,独享,互斥,可重入的重量级锁。

Lock锁都在JUC包下的类和接口,是API层面上的实现:
• ReentrantLock是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
• ReentrantReadWriteLocK是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

再来说说Synchronized和Lock的对比:

1. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。


那如何对Lock和synchronized进行选择呢?

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;(I/O和Synchronized都能相应中断,即不需要处理interruptionException异常)
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。


好了,在java中锁的基本介绍和主要使用的两种锁机制都已经介绍了,下面本篇将主要介绍synchronized的使用和机制,所以如果你对它感兴趣就接着往下看,如果对Lock感兴趣就可以绕路去查看其他资料了。。。

虽然是synchronized专场,还是先来给大家告知一下synchronized关键字加锁的缺陷


如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。
  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程就只能在那等待了,可想而知,这多么影响程序执行效率。
  再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
  但是采用synchronized关键字来实现同步的话,就会导致一个问题:
  如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。


ok,虽然synchronized有自己的不足,但是它还是咱们对程序加锁的常用手段,所以接下来的内容就是用一些例子来介绍synchronized关键字怎么加锁代码的执行的。

synchronized关键字基本有以下两种用法:
1、 在需要同步的方法的方法签名中加入synchronized关键字。
 synchronized public void getValue() {
                        System.out.println("getValue method thread name="
                                                        + Thread.currentThread().getName() + " username=" + username
                                                        + " password=" + password);
                       }
上面的代码修饰的synchronized是非静态方法。。。


 synchronized static public void getValue() {
                        System.out.println("getValue method thread name="
                                                       + Thread.currentThread().getName() + " username=" + username
                                                       + " password=" + password);
                        }

上面的代码修饰的synchronized是静态方法。。。
2、使用synchronized块对需要进行同步的代码段进行同步。
 public void serviceMethod() {
             try {
                    synchronized (this) {
                                  System.out.println("begin time=" + System.currentTimeMillis());
                                  Thread.sleep(2000);
                                  System.out.println("end end=" + System.currentTimeMillis());
                  }
            } catch (InterruptedException e) {
                        e.printStackTrace();
            }
}
上面的代码块是synchronized (this)用法。。。

其实:在Java中,多线程的线程同步机制实际上是靠锁的概念来控制的,synchronized关键字同步的是 对象锁和类锁。

在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:
1. 保存在堆中的实例变量
2. 保存在方法区中的类变量
因为这两类数据是被所有线程共享的。(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)

友情提示:线程访问实例变量或者类变量不需锁!!

对于java语言,编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。


总体来说程序对锁定代码的执行是:一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);
如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);
线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。


以下全部就是synchronized的用法与实例

简言:synchronized修饰非静态方法、同步代码块的synchronized (this)用法和synchronized (非this对象)的用法锁的是对象,线程想要执行对应同步代码,需要获得对象锁。
synchronized修饰非静态方法以及同步代码块的synchronized (类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
以下用几个代码例子来演示一下synchronized的用法:


非线程安全实例:

public class Run {

public static void main(String[] args) {

               HasSelfPrivateNum numRef = new HasSelfPrivateNum();

               ThreadA athread = new ThreadA(numRef);
               athread.start();

               ThreadB bthread = new ThreadB(numRef);
               bthread.start();

          }

}

class HasSelfPrivateNum {

        private int num = 0;

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

}

class ThreadA extends Thread {

        private HasSelfPrivateNum numRef;

        public ThreadA(HasSelfPrivateNum numRef) {
            super();
            this.numRef = numRef;
        }

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

}

class ThreadB extends Thread {

        private HasSelfPrivateNum numRef;

        public ThreadB(HasSelfPrivateNum numRef) {
            super();
            this.numRef = numRef;
        }

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

}
运行结果为:
a set over!
b set over!
b num=200
a num=200


将HasSelfPrivateNum类如下,方法用synchronized修饰从而实现线程的同步效果:


class HasSelfPrivateNum {

    private int num = 0;

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

}
运行结果是线程安全的:

b set over!
b num=200
a set over!
a num=100
结论:两个线程访问同一个对象中的同步方法是一定是线程安全的。本实现由于是同步访问,所以先打印出a,然后打印出b。
这里线程获取的是HasSelfPrivateNum的对象实例的锁——对象锁。

多个对象有多个锁
public class Run {

    public static void main(String[] args) {

            HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
            HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();

            ThreadA athread = new ThreadA(numRef1);
            athread.start();

            ThreadB bthread = new ThreadB(numRef2);
            bthread.start();

    }

}
运行结果为:
a set over!
b set over!
b num=200
a num=100

对于多个对象来说它们拥有多个锁,每个对象都有自己的锁,所以这里是非同步的,因为线程athread获得是numRef1的对象锁,而bthread线程获取的是numRef2的对象锁,他们并没有在获取锁上有竞争关系,因此,出现非同步的结果。

同步代码块synchronized (this)
 public class Run {

     public static void main(String[] args) {
            ObjectService service = new ObjectService();

            ThreadA a = new ThreadA(service);
            a.setName("a");
            a.start();

            ThreadB b = new ThreadB(service);
            b.setName("b");
            b.start();
    }

}

class ObjectService {

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

class ThreadA extends Thread {

    private ObjectService service;

    public ThreadA(ObjectService service) {
        super();
        this.service = service;
    }

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

}

class ThreadB extends Thread {
    private ObjectService service;

    public ThreadB(ObjectService service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.serviceMethod();
    }
}
运行结果:
begin time=1466148260341
end end=1466148262342
begin time=1466148262342
end end=1466148264378


这样也是同步的,线程获取的是同步块synchronized (this)括号()里面的对象实例的对象锁,这里就是ObjectService实例对象的对象锁了。


友情提示:需要注意的是synchronized (){}的{}前后的代码依旧是异步的。


同步代码块synchronized (非this对象)
public class Run {

    public static void main(String[] args) {

        Service service = new Service("xiaobaoge");

        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();

}

}

class Service {

    String anyString = new String();

    public Service(String anyString){
        this.anyString = anyString;
    }

    public void setUsernamePassword(String username, String password) {
        try {
            synchronized (anyString) {
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                                            + "在" + System.currentTimeMillis() + "进入同步块");
                Thread.sleep(3000);
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                                            + "在" + System.currentTimeMillis() + "离开同步块");
            }
            } catch (InterruptedException e) {
                        e.printStackTrace();
}
}

}

class ThreadA extends Thread {
    private Service service;

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

    @Override
    public void run() {
        service.setUsernamePassword("a", "aa");

    }

}

class ThreadB extends Thread {

    private Service service;

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

    @Override
    public void run() {
        service.setUsernamePassword("b", "bb");

    }

}
不难看出,这里线程争夺的是anyString的对象锁,两个线程有竞争同一对象锁的关系,出现同步。


请特别注意:如果对象实例A,线程1获得了对象A的对象锁,那么其他线程就不能进入需要获得对象实例A的对象锁才能访问的同步代码(包括同步方法和同步块)。

synchronized同步静态方法
public class Run {

    public static void main(String[] args) {

        ThreadA a = new ThreadA();
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB();
        b.setName("B");
        b.start();

    }

}

class Service {

      synchronized public static void printA() {
        try {
        System.out.println("线程名称为:" + Thread.currentThread().getName()
                                    + "在" + System.currentTimeMillis() + "进入printA");
        Thread.sleep(3000);
        System.out.println("线程名称为:" + Thread.currentThread().getName()
                                    + "在" + System.currentTimeMillis() + "离开printA");
            } catch (InterruptedException e) {
                e.printStackTrace();
    }
}

    synchronized public static void printB() {
            System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
                                        + System.currentTimeMillis() + "进入printB");
            System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"
                                        + System.currentTimeMillis() + "离开printB");
        }

}

class ThreadA extends Thread {
    @Override
    public void run() {
        Service.printA();
    }

}

class ThreadB extends Thread {
    @Override
    public void run() {
        Service.printB();
    }
}
运行结果:
线程名称为:A在1466149372909进入printA
线程名称为:A在1466149375920离开printA
线程名称为:B在1466149375920进入printB
线程名称为:B在1466149375920离开printB


可以看出来,两个线程在争夺同一个类锁,因此代码的执行被同步。


synchronized同步代码块 (class类锁)
class Service {

    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                                            + "在" + System.currentTimeMillis() + "进入printA");
                Thread.sleep(3000);
                System.out.println("线程名称为:" + Thread.currentThread().getName()
                                            + "在" + System.currentTimeMillis() + "离开printA");
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }
    }

}

public static void printB() {
    synchronized (Service.class) {
            System.out.println("线程名称为:" + Thread.currentThread().getName()
                                        + "在" + System.currentTimeMillis() + "进入printB");
            System.out.println("线程名称为:" + Thread.currentThread().getName()
                                        + "在" + System.currentTimeMillis() + "离开printB");
            }
    }
}
运行结果:
线程名称为:A在1466149372909进入printA
线程名称为:A在1466149375920离开printA
线程名称为:B在1466149375920进入printB
线程名称为:B在1466149375920离开printB


因为两个线程依然是在争夺同一个类锁,因此代码执行被同步。


这里需要特别说明:对于同一个类A,线程1争夺A对象实例的对象锁,线程2争夺类A的类锁,这两者不存在竞争关系。也就说对象锁和类锁互不干涉锁的抢夺和代码的执行。

最后补充一点:加上类锁是对该类的所有对象都能起作用,而对象锁不能,对象锁只能对该单个对象起作用。

好了,到了这里,这篇java的同步锁就介绍完了,希望对你有所帮助,see you


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值