线程间的共享 synchronized 和 volatile 关键字

Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

Java 多线程中的同步机制会对资源进行加锁,保证同一时间只有一个线程可以操作对应资源,避免多线程同时访问相同资源发生冲突。Synchronized 是 Java 中的关键字,是一种同步锁,可以实现同步机制。

Synchronized 修饰的对象有一下几种:

  1. 修饰一个类。作用范围是 Synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象。
  2. 修饰普通方法。被修饰的方法称为同步方法,作用范围是整个方法。一个对象中的加锁方法只允许一个线程访问,但这种情况下锁的是访问该方法的实例对象,如果多个线程不同对象访问该方法,则无法保证同步。
  3. 修饰静态方法。由于静态方法是类方法,所以锁的是包含该方法的类,也就是类对象;如果多个线程不同对象访问该静态方法,可以保证同步。
  4. 修饰代码块。普通代码块如 Synchronized(obj),obj 可以为类中的一个属性,也可以是当前的对象,同步效果和修饰普通方法一样; Synchronized(obj.class) 静态代码块的同步效果和修饰静态方法类似。
  • Synchronized 方法控制的范围较大,它会同步对象中的所有 Synchronized 方法的代码
  • Synchronized 代码块控制范围较小,只会同步代码块中的代码,而位于代码块之外的代码可以被多个线程访问,Synchronized代码块更加灵活精确。
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。

示例:

package com.wuc.generic.thread;

public class SynTest {
    private long count = 0;
    private Object obj = new Object();//作为一个锁

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    /*用在同步块上*/
    public void incCount() {
        synchronized (obj) {
            count++;
        }
    }

    /*用在方法上*/
    public synchronized void incCount2() {
        count++;
    }

    /*用在同步块上,但是锁的是当前类的对象实例*/
    public void incCount3() {
        synchronized (this) {
            count++;
        }
    }

    //线程
    private static class Count extends Thread {

        private SynTest simplOper;

        public Count(SynTest simplOper) {
            this.simplOper = simplOper;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                simplOper.incCount();//count = count+10000
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynTest simplOper = new SynTest();
        //启动两个线程
        Count count1 = new Count(simplOper);
        Count count2 = new Count(simplOper);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(simplOper.count);//20000
    }
}

修饰类

其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象,如下代码:

class ClassName {
      public void method() {
         synchronized (ClassName.class) {
             //todo
         }
     }
 }

修饰普通方法

Synchronized 修饰一个方法就是在方法签名加 Synchronized,如下代码:

public synchronized void method() {
}

在定义接口方法时不能使用 Synchronized 关键字
构造方法不能使用 Synchronized 关键字,但可以使用 Synchronized 代码块来进行同步
Synchronized 不能被继承。如果子类覆盖了父类的被 Synchronized 关键字修饰的方法,那么子类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。

修饰静态方法

静态方法属于类而不属于对象,所以 Synchronized 修饰的静态方法锁定的是这个类的所有对象。

 public synchronized static void method() {
 }

错误的加锁和原因分析

代码示例:

// 类说明:错误的加锁和原因分析
public class TestIntegerSyn {

    public static void main(String[] args) throws InterruptedException {
        Worker worker = new Worker(1);
        //Thread.sleep(50);
        for (int i = 0; i < 5; i++) {
            new Thread(worker).start();
        }
    }

    private static class Worker implements Runnable {

        private Integer i;
        private Object o = new Object();

        public Worker(Integer i) {
            this.i = i;
        }

        @Override
        public void run() {
            synchronized (i) {
                Thread thread = Thread.currentThread();
//                System.out.println(thread.getName() + "--@"
//                        + System.identityHashCode(i));
                i++;
                System.out.println(thread.getName() + "-------" + i + "-@"
                        + System.identityHashCode(i));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + "-------" + i + "--@"
                        + System.identityHashCode(i));
            }
        }
    }
}

运行结果:

可以看出结果并没有按预计的 2,3,4,5,6输出。

原因:虽然我们对 i 进行了加锁,但是

但是当我们反编译这个类的 class 文件后,可以看到 i++实际是

Integer 对象源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

本质上是返回了一个新的 Integer 对象。也就是每个线程实际加锁的是不同的 Integer 对象。synchronized (i) 锁的对象 i 是变化的,故结果错误。

解决:
synchronized (i) 改为 synchronized (o) 即可。锁的对象o是不变的。

synchronized 的不足之处

  • 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,同一时间只能有一个线程执行
  • synchronized 无法知道线程有没有成功获取到锁
  • 使用 synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待

volatile 最轻量的同步机制

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。
参见代码:

/**
 * 类说明:演示Volatile的提供的可见性
 */
public class VolatileCase {
    private volatile static boolean ready;
    private static int number;

    private static class PrintThread extends Thread {
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while (!ready) ;//无限循环
            System.out.println("number = " + number);
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        SleepTools.second(1);
        number = 51;
        ready = true;
        SleepTools.second(5);
        System.out.println("main is ended!");
    }
}

不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环, 而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。 但是 volatile 不能保证数据在多个线程下同时写时的线程安全

不加 volatile 时效果图:

加 volatile 时效果图:

volatile 不能保证数据在多个线程下同时写时的线程安全,参见代码:

public class NotSafe {
    private volatile long count = 0;

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    //count进行累加
    public void incCount() {
        count++;
    }

    //线程
    private static class Count extends Thread {

        private NotSafe simplOper;

        public Count(NotSafe simplOper) {
            this.simplOper = simplOper;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                simplOper.incCount();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NotSafe simplOper = new NotSafe();
        //启动两个线程
        Count count1 = new Count(simplOper);
        Count count2 = new Count(simplOper);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(simplOper.count);//20000?
    }
}

volatile 最适用的场景:一个线程写,多个线程读。

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

推荐阅读更多精彩内容