synchronized / Lock+volatile

最近在学习单例模式和Android消息传递方面的知识,都用到了synchronized同步关键字,于是整理下思路。

由于同一进程内线程共享同一片内存单元,当多线程进行读写的时候就会存在冲突的问题。Java提供了synchronized和Lock来实现同步互斥访问,有效的避免了一个临界数据同时被多个线程同时访问可能出现的错误。

synchronized

synchronized是java中的一个关键字,是Java语言内置的特性。synchronized的使用主要有2种:同步方法和同步代码块。

使用synchronized需要明确的几个问题:

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁(lock)与之相关联。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

synchronized方法

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。synchronized修饰方法又分为修饰静态方法和修饰非静态方法。

synchronized修饰非静态方法

具体看下面例子:

public class SynchronizedTest {
    public static void main(String[] args){
        Test t1 = new Test();
        t1.start();
        Test t2 = new Test();
        t2.start(); 
    }
}

class Test extends Thread{
    @Override
    public void run() { 
        writeSomething(); 
    } 
    public synchronized void writeSomething(){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        }
    } 
    public void printSomething(){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        } 
    }
}

输出结果如下:

0 0 1 1 2 2 3 4 3 5 4 6 7 8 9 5
6 7 8 9 
//这里有一个换行

Test类的writeSomething方法加了synchronized可是没有像预期那样输出俩行0-9,这是为什么呢?因为上例中synchronized用来修饰非静态方法,而非静态方法又是类对象所有,所以在不同对象的writeSomething()方法互不干扰。这对不对呢,我们在试一下就知道了。测试代码如下:

public class SynchronizedTest {
    static Test test1 = new Test();
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.writeSomething();
            }
        }).start();
        test1.writeSomething(); //这一句要放在new Thread后面,不然会把这一句执行完才执行new Thread
    }
}

class Test{ 
    public synchronized void writeSomething(){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        } 
        System.out.println();
    }
}

new一个线程和主线程都执行test1的writeSomething方法,输出结果如下:

0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 
//这里有一个换行

可以看到同一对象synchronized关键字起作用了,说明了synchronized修饰非静态方法,是作用在同一对象上的。下面还有个例子:

public class SynchronizedTest {
    static Test test1 = new Test();

    public static void main(String[] args){
        new Thread(new Runnable() {

            @Override
            public void run() {
                test1.writeSomething();
            }
        }).start();

        test1.printSomething(); //这一句要放在new Thread后面,不然会把这一句执行完才执行new Thread
    }
}

class Test{
    public synchronized void writeSomething(){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        }
        System.out.println();
    } 

    public synchronized void printSomething(){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        } 
    System.out.println(); 
    }
}

这个例子在上面的基础上多加了一个同步方法,猜想一下,正常情况下,不同线程执行不同的方法,应该是交叉执行打印的,先看下输出结果:

0 1 2 3 4 5 6 7 8 9
 0 1 2 3 4 5 6 7 8 9
 //换行

输出结果跟预期的不一样,输出2行0-9说明是按照先后的顺序执行的(不放心的话可以多执行几次)为什么会这样呢?前面提到每一个对象只有一锁与之对应,当执行test1.writeSomething()时相当于当前线程拿到了test1的锁,而其他线程只有等待它释放锁才能继续执行。而后面的test1.printSomething()方法继续执行正需要这个锁。所以它就必须等到前面test1.writeSomething()执行完,释放了锁以后拿到锁才能继续执行,所以就有了这样的输出结果。当一个线程访问object的一个synchronized同步方法时,其他线程对object中所有其它synchronized同步方法的的访问将被阻塞。

synchronized修饰静态方法

修饰静态方法没什么好解释的,因为静态方法不属于类对象,它是属于类的,所以如果用synchronized修饰静态方法,那么它在所有类对象中都是同步的。


synchronized代码块

我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。因为当方法体过于庞大而需要同步的部分又很少,锁的时间就加长了,别的线程是不是要等很久。所以往往同步代码块比同步方法好用。synchronized代码块又分为这么几种:synchronized(this),synchronized(className.class)和synchronized(Object obj)。

synchronized(this)

synchronized(this)类似于前面的synchronized修饰非静态方法,锁都在当前对象,只限制当前对象对该代码块的同步。

public synchronized void writeSomething(){
    //其他代码
    synchronized (this){
        for (int i=0; i<10; i++){
            System.out.print(i+" ");
        }
    System.out.println();
    }
    //其他代码
}

synchronized(className.class)

synchronized(className.class)类似于前面的synchronized修饰静态方法,锁在类而不在类对象,只要是className类对象访问该代码块都被要求同步。

class Test {
    public synchronized void writeSomething() {
        //其他代码
        synchronized (Test.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
            System.out.println();
        }
        //其他代码
    }
}

synchronized(Object obj)

这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Test {
    private static byte[] lock = new byte[0]; // 特殊的instance变量 public

    synchronized void writeSomething() {
        //其他代码 
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
            System.out.println(); 
        }
        //其他代码
    }
}

(tips:用的比较多的就是零长度的byte数组对象,创建起来将比任何对象都经济。查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。)

这里的锁的作用范围取决于lock的作用域,谁能拿到这个lock就能访问该代码块。比如将lock作为staic全局变量就是类所有,这时synchronized (lock)就相当于synchronized (className.class);相反,将lock作为局部变量(放在方法内)该synchronized 将失效,因为每个访问该方法的都能获得一个lock对象。

Lock

之前在面试中被问过相关问题,所有之后就花时间了解了下。Lock和synchronized 不同,synchronized 会自动释放锁,而Lock必须手动释放,如果没有释放就可能造成死锁。并且Lock的使用一般放在try{}catch块中,最后在finally中释放锁,保证抛出异常时锁会被释放。点开Lock的源码可以看到,Lock是一个接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock(); 
    Condition newCondition();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()返回的是一个Condition对象,关于这个类我翻了翻api表示看不懂。

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()都是用来获取锁的,那他们有什么区别呢?
lock()是使用的最多的,它就是用来获取锁,如果锁被其他线程拿到,它就等待。
tryLock()是有返回值的,尝试获取锁,成功就返回true失败就返回false。所以说这个方法无论拿不拿得到锁都会立即返回而不会在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。


volatile

volatile的使用场景,通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。volatile关键字会强制将修改的值立即写入主存,使线程的工作内存中缓存变量行无效。
  • 禁止进行指令重排序。

在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据。

  • 对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
  • 对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。我第一次接触到volatile关键字是在双重锁的单例模式中
public class Singleton {

    private volatile static Singleton sSingleton;  

    private Singleton (){}  

    public static Singleton getSingleton() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }
         }  
         return sSingleton;
      }
}

当时想了蛮久为什么要判断2次不为空,所以印象蛮深刻的。这是因为如果没有volatile关键字,问题可能会出在singleton = new Singleton();这句,用伪代码表示

inst = allocat();  // 分配内存 
sSingleton = inst; // 赋值
constructor(inst);;// 真正执行构造函数

可能会由于虚拟机的优化等导致赋值操作先执行,而构造函数还没完成,导致其他线程访问得到singleton变量不为null,但初始化还未完成,导致程序崩溃。


synchronized的不足

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

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。


Lock与synchronized的不同:

  • Lock支持在等待一定的时间或者能够响应中断。
  • Lock支持在多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
  • 通过Lock可以知道线程有没有成功获取到锁。
  • Lock不是Java语言内置的。synchronized是Java语言的关键字,因此是内置特性。
  • Lock是一个类,通过这个类可以实现同步访问。
  • Lock必须要用户去手动释放锁,而synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用。

参考链接:
Java synchronized详解
Java并发编程:Lock
Synchronized/Lock/Volatile
单例模式,你知道的和你所不一定知道的一切

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,690评论 0 11
  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,627评论 0 11
  • 在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。对临界资源加上互斥锁,当一个线...
    米刀灵阅读 1,032评论 0 6
  • 杰克是个孤独又懦弱的小孩、因为从小被爷爷灌输的故事,他一度被所有人当成是个有精神疾病的孩子。直到爷爷去世、他循...
    梓梦柔阅读 208评论 0 0
  • 宿舍内静静的,就我一个人醒来,阳光透过窗户闯了进来,但却驱散不了清晨的黑暗,我慢悠悠的下床洗漱,这让整个宿舍都传出...
    孤境残阳阅读 203评论 2 0