目录:

  • 什么是synchronized?
  • synchronized和原子性、可见性和有序性之间的关系
  • synchronized的几种用法
  • synchronized与lock的区别

1. 什么是synchronized

synchronized中文意为:同步的,同步化的。是Java中的一个关键字。

常用作给方法或者代码块加锁。加锁后,同一时刻只能有一个线程执行这段代码。以此来保证线程安全。

2. synchronized和原子性、可见性和有序性之间的关系

先简单理解下3个概念,

  • 原子性(Atomic):原子(atom)本意指化学反应不可再分的基本微粒。在编程中原子性指一个操作不可再被分隔成多步。一个操作或者多个操作 要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序的执行顺序按照代码的先后顺序执行。

为什么程序的执行顺序有时会不按照代码的先后顺序执行呢?

这里面涉及到指令重排序(Instruction Reorder)的概念。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序的结果不会影响到单线程的执行,但不能保证多线程并发执行时不受影响。

例如以下代码在未发生指令重排序时,其执行顺序为1->2->3->4。但在真正执行时,将可能变为1->2->4->3或者2->1->3->4或者其他。但其会保证1处于3之前,2处于4之前。所有最终结果都是a=10; b=20

int a = 0;//语句1
int b = 1;//语句2
a = 10; //语句3
b = 20; //语句4
复制代码

但如果是多线程情况下,另一个线程中有以下程序。当上述的执行顺序被重排序为1->2->4->3,当线程1执行到第3步b=20时,切换到线程2执行,其会输出a此时已经是10了,而此时a的值其实还是为0。

if(b == 20){
  System.out.print("a此时已经是10了");
}

复制代码

2.1 synchronized与原子性的关系?

被synchronized关键字包裹起来的方法或者代码块可以认为是原子的。因为在锁未释放之前,这段代码无法被其他线程访问到,所以从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。

在Java中,synchronized对应着两个字节码指令monitorentermonitorexit。通过monitorentermonitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。

2.2 synchronized是如何保证可见性的?

根据JMM(Java Memory Model,Java内存模型)机制,内存主要分为主内存和工作内存两种,线程工作时会从主内存中拷贝一份变量到工作内存中。

JMM对synchronized做了2条规定:

  1. 线程解锁前,必须把变量的最新值刷新到主内存中。
  2. 线程加锁时,先清空工作内存中的变量值,从主内存中重新获取最新值到工作内存中。

2.3 synchronized可以保证有序性吗?

synchronized可以保证一定程度的有序性,但其是不能禁止指令重排序的,synchronized 代码块里的非原子操作依旧可能发生指令重排。

具体怎么理解呢?

  • 这里要先说一个概念,as-if-serial语义,其是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义
  • as-if-serial语义保证了单线程中指令重排序是有一些限制的,即无论怎么重排序,都不能影响到单线程执行的结果。而synchronized保证了这一块程序在同一时间内只能被同一线程访问,所以其也算是保证了有序性。

3. synchronized的几种用法

synchronized的用法大概可以分为3种,

  1. 用来修饰普通方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁
  2. 用来修饰静态方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁
  3. 作用于代码块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

其作用在方法上的写法如下图,synchronized只要放在返回类型前面就行。

 

 

下面将举一些具体的实例,来看下使用synchronized后的结果:

3.1 synchronized作用于实例方法

public class TestBean {
    //TestBean中有两个实例方法,method1和method2
    public synchronized void method1(){
        System.out.println("method1 start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public synchronized void method2(){
        System.out.println("method2 start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }
}
复制代码
public class MainTest {

    public static void main(String[] args){
        TestBean testBean = new TestBean();
        //第一个线程,执行method1()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean.method1();
            }
        }).start();
        /立刻开启第二个线程,执行method2()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean.method2();
            }
        }).start();
    }
}
复制代码

控制台结果:

method1 start
(...3秒后输出)
method1 end
method2 start
(...3秒后输出)
method2 end
复制代码

可以看出锁作用于testBean对象上,其他线程来访问synchronized修饰的其他方法时需要等待线程1先把锁释放。

  • 那如果method2()synchronized修饰符去掉呢。那自然是不用等锁释放,就会立刻执行method2()。控制台输出以下结果:
method1 start
method2 start
(...3秒后输出)
method1 end
method2 end
复制代码
  • 又或者TestBean.java类保持不变,两个方法均由synchronized修饰,但有两个不同的实例对象。
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();
        
        //第一个线程,执行method1()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method1();
            }
        }).start();
        /立刻开启第二个线程,执行method2()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method2();
            }
        }).start();
    }
}
复制代码

此时因为两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响,控制台的结果如下:

method1 start
method2 start
(...3秒后输出)
method1 end
method2 end
复制代码

3.2 synchronized修饰静态方法

将上文中的TestBean.java改为:

public class TestBean {
    //TestBean中有一个静态方法,method
    synchronized public static void method(String threadName){
        System.out.println("method start by " + threadName);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method end by " + threadName);
    }
}
复制代码

Main.java如下:

public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method("thread1");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method("thread2");
            }
        }).start();
    }
}
复制代码

控制台结果:

method start by thread1
(...3秒后输出)
method end by thread1
method start by thread2
(...3秒后输出)
method end by thread2
复制代码

分析:由例子可知,两个线程虽然使用的是两个不同的对象,但是访问的方法是静态的,两个线程最终还是发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。

3.3 synchronized修饰代码块

为什么要作用于代码块呢?

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,毕竟长锁不如短锁,尽可能只锁必要的部分。

public class TestBean {

    private final static Object objectLock = new Object();

    void method1(){
        System.out.println("not synchronized method1");
        synchronized (objectLock){
            System.out.println("synchronized method1 start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("synchronized method1 end  ");
        }

    }

    void method2(){
        System.out.println("not synchronized method2" );
        synchronized (objectLock){
            System.out.println("synchronized method2 start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("synchronized method2 end");
        }
    }
}
复制代码
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method2();
            }
        }).start();
    }
}
复制代码

控制台输出:

not synchronized method1
synchronized method1 start
not synchronized method2
(...3秒后输出)
synchronized method1 end  
synchronized method2 start
(...3秒后输出)
synchronized method2 end
复制代码

可以看到未被synchronized包裹的代码时不存在互斥的,System.out.println("not synchronized method2" ); 无需等到method1执行完成,而被synchronized包裹的代码块,且使用了同一个对象作为锁的话,那就互斥了。

结论:

  • synchronized作用于一个给定的实例对象objectLock,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有objectLock实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。
  • 当然除了objectLock作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
    synchronized (this){
        System.out.println("synchronized method1 start");
        System.out.println("synchronized method1 end");
    }

    synchronized (TestBean.class){
        System.out.println("synchronized method2 start");
        System.out.println("synchronized method2 end");
    }
复制代码

4. synchronized和lock之间的区别

Lock是Java语言中的一个接口类,对应的实现类为ReentrantLock,它们都位于java.util.concurrent.locks包下,熟悉Java的同学肯定都知道concurrent包下都是用于处理Java多线程问题的类。

4.1 lock的使用

lock的几个常用方法:

lock():获取锁,如果锁被暂用则一直等待

unlock():释放锁

tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true

tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
ck
lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
复制代码

lock的使用与synchronized作用于代码块时类似:

public class TestBean {
    private Lock lock = new ReentrantLock();
    void method1(String threadName) {
        lock.lock();
        try{
            System.out.println("method1 start " + threadName);
            //耗时操作
            ...
        } finally {
            System.out.println("method1 end " + threadName); 
            lock.unlock();//释放锁
        }
    }

}
复制代码
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();

        new Thread("thread1") {
            @Override
            public void run() {
                testBean1.method1(Thread.currentThread().getName());
            }
        }.start();

        new Thread("thread2"){
            @Override
            public void run() {
                testBean1.method1(Thread.currentThread().getName());
            }
        }.start();
    }
}
复制代码

执行结果如下:

method1 start thread1
(...3秒后输出)
method1 end thread1
method1 start thread2
(...3秒后输出)
method1 end thread2
复制代码

注意:使用lock时,需要在finally中释放锁lock.unlock();,不然可能会造成死锁。

  • 将TestBean.java稍微改动,来看下tryLock()方法的使用:
public class TestBean {
    private Lock lock = new ReentrantLock();
    void method1(String threadName) {
         if (lock.tryLock()){
            try{
                System.out.println("method1 start " + threadName);
                //耗时操作
                ...
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("我是"+threadName+",有人占着锁,我放弃了");
        }
    }

}
复制代码

控制台输出:

method1 start thread1
我是thread2,有人占着锁,我放弃了
(...3秒后输出)
method1 end thread1
复制代码

4.2 lock与synchronized的区别

类别 synchorinzed lock
存在层次 Java的关键字 接口类,由ReentrantLock实现
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,可尝试获得锁tryLock(),线程可以不用一直等待
锁的状态 无法判断 可判断
性能 资源竞争不是很激烈的情况下,比较合适的;当同步非常激烈的时候,synchronized的性能会一下子能下降得很快 在资源竞争不激烈的情形下,性能稍微比synchronized差点;但其在资源竞争激烈时,可维持常态

两者具体的性能测试:www.cnblogs.com/nsw2018/p/5…

参考:blog.csdn.net/u012403290/…

结语

之后会写一篇文章描述下synchronizeds的底层原理: 涉及到的概念会有Monitor、monitorenter和monitorexit指令、synchronized的可重入性、synchronized与中断。

另外会写一篇关于volatile的文章:主要涉及Volatile的特性、其实现原理、指令重排序与内存屏障