转载

Java多线程之Synchronized

作者某人Valar

如需转载请保留原文链接

目录:

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

1. 什么是synchronized

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

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

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

先简单理解下3个概念,

原子(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 只要放在 返回类型 前面就行。

Java多线程之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
复制代码
原文  https://juejin.im/post/5e01be596fb9a0161d743cd0
正文到此结束
Loading...