【Java并发编程】2.1对象及变量的并发访问——synchronized同步方法

前言

着重掌握如下技术点:
1.synchronized 对象监视器为Object的使用;
2.synchronized 对象监视器为Class时的使用;
3.非线程安全时如何出现的;
4.关机字volatile的主要作用;
5.关键字volatile与synchronized的区别与使用情况。

synchronized同步方法

非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据是被更改过的。
而线程安全就是已获得的实例变量的值是经过同步处理的,不会出现脏读的线程。

2.1.1方法内的变量为线程安全

“非线程安全”问题存在于“实例变量中”,如果是方法内部的私有变量,则不存在“非线程安全问题”,所以结果也是线程安全的。
下面例子实现方法内部声明一个变量,是不存在非线程安全问题的。

public class HasSelfPrivateNum {

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

class ThreadA extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class ThreadB extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class Run{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

结果为:

b set off
b num=200
a set off
a num=100

可见方法中的内部变量num所执行的操作 不存在非线程安全问题。

2.1.2实例变量非线程安全

如果多个线程共同访问一个对象中的实例变量就有可能出现“非线程安全问题”。
用线程访问的对象中如果有多个实例变量,则运行结果有可能出现交叉的情况。
如果对象中仅有一个实例变量,则有可能出现覆盖。

/**
 * 实例变量num被多个线程访问
 * @author Arthur
 * @date 2017-12-25 14:15
 */

public class HasSelfPrivateNum {

    private int num =0;

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

class ThreadA extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class ThreadB extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class Run{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}
a set off
b set off
b num=200
a num=200

本例子是两个线程同时访问一个没用同步的的方法,如果两个线程同时操作业务对象中的实例变量,则会出现“非线程安全”问题。解决方案是只要在方法前加synchronized关键字即可。在两个线程访问同一个对象中的同步方法时一定是线程安全的。

2.1.3多个对象多个锁

public class HasSelfPrivateNum {

    private int num = 0;

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

class ThreadA extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class ThreadB extends Thread {

    private HasSelfPrivateNum numRef;

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

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

class Run{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef2);
        threadB.start();
    }
}
a set off
b set off
b num=200
a num=100

上述代码由于创建了2个HasSelfPrivateNum 实例,拥有了两个锁,虽然方法是同步的,但是造成的结果却是异步的。效果是先打印出b的值 再打印出a的值。
为什么是这样的呢?synchronized关键字是对对象加锁,而不是对方法或代码块加锁,上述例子中哪个线程先执行带synchronized的方法,哪个线程就先获得该方法所属对象的锁Lock,其他线程等待,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,JVM就会创建多个锁。上述问题就是如此。

2.1.4synchronized方法与锁对象

为了验证线程锁的是对象,代码如下:

/**
 * 验证线程锁的是对象
 *
 * @author Arthur
 * @date 2017-12-26 16:08
 */

public class SynchronizedMethodLock {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        MyThreadA myThreadA = new MyThreadA(myObject);
        MyThreadB myThreadB = new MyThreadB(myObject);
        myThreadA.setName("A");
        myThreadB.setName("B");
        myThreadA.start();
        myThreadB.start();
    }
}

class MyObject{
    public void method() throws InterruptedException {
        System.out.println("begin threadName:"+Thread.currentThread().getName());
        Thread.sleep(5000);
        System.out.println("end");
    }
}
class MyThreadA extends Thread{

    private MyObject myObject;

    public MyThreadA(MyObject myObject) {
        this.myObject = myObject;
    }

    @Override
    public void run() {
        super.run();
        try {
            myObject.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThreadB extends Thread{

    private MyObject myObject;

    public MyThreadB(MyObject myObject) {
        this.myObject = myObject;
    }

    @Override
    public void run() {
        super.run();
        try {
            myObject.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果为

begin threadName:A
begin threadName:B
A end
B end

当method()方法用synchronized修饰时

public synchronized void method() 

运行结果为

begin threadName:A
A end
begin threadName:B
B end

可见调用关键字synchronized声明的方法一定是排队运行的。另外只有共享的资源的读写访问才需要同步化处理!
那其他方法在被调用时是什么效果呢?如何查看Lock锁对象的效果呢?更新代码如下:

/**
 * 验证线程锁的是对象
 *
 * @author Arthur
 * @date 2017-12-26 16:08
 */

public class SynchronizedMethodLock {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        MyThreadA myThreadA = new MyThreadA(myObject);
        MyThreadB myThreadB = new MyThreadB(myObject);
        myThreadA.setName("A");
        myThreadB.setName("B");
        myThreadA.start();
        myThreadB.start();
    }
}

class MyObject{
    public /*synchronized*/ void method() throws InterruptedException {
        System.out.println("begin threadName:"+Thread.currentThread().getName());
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+" end");
    }
}
class MyThreadA extends Thread{

    private MyObject myObject;

    public MyThreadA(MyObject myObject) {
        this.myObject = myObject;
    }

    @Override
    public void run() {
        super.run();
        try {
            myObject.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThreadB extends Thread{

    private MyObject myObject;

    public MyThreadB(MyObject myObject) {
        this.myObject = myObject;
    }

    @Override
    public void run() {
        super.run();
        try {
            myObject.method();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果如下

begin threadName:A
begin threadName:B
A end
B end

可以看到即使methodA为同步方法,线程A持有了myObject对象的锁,但是线程B仍能够异步调用非synchronized方法methodB。
继续试验,在methodB方法上也加上synchronized

public synchronized void methodB() 

运行结果为按顺序执行。

begin methodA threadName:A
A end
begin methodB threadName:B
B end

以此可以得出结论
1)A线程先持有object对象的锁,B线程可以以异步的方法调用object对象中非synchronized的方法。
2)A线程先持有object对象的锁,B线程如果这时调用object对象中的synchronized的方法那么需要等待,也就是同步。

2.1.5脏读

前面说明了在多个线程调用同一方法时,为了避免数据交叉的情况,用synchroinized同步。但是有些操作虽然在赋值时进行了同步,但在取值的时候可能出现意外,这种情况就是脏读。发生脏读的情况是在读取实例变量的时候,此值已经被其他线程更改过了。

/**
 * 脏读现象
 *
 * @author Arthur
 * @date 2017-12-26 17:07
 */

public class Run {
    public static void main(String[] args) throws InterruptedException {
        PublicVar varRef = new PublicVar();
        ThreadA threadA = new ThreadA(varRef);
        threadA.start();
        threadA.setName("threadA");
        Thread.sleep(200);//打印结果受此值大小影响
        varRef.getValue();
    }
}
class PublicVar{
    public String username = "A";
    public String password = "AA";

    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;
            System.out.println("setValue method threadName="+Thread.currentThread().getName()+" username="+username+" password="+password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

class ThreadA extends Thread{
    private PublicVar var;

    public ThreadA(PublicVar var) {
        this.var = var;
    }

    @Override
    public void run() {
        super.run();
        var.setValue("B","BB");
    }
}

程序运行为:

getValue method threadName=main username=B password=AA
setValue method threadName=threadA username=B password=BB

出现脏读是因为public void getValue()方法不是同步的,所以在任意时刻都可以调用。
解决方案是加上同步关键字

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

程序运行正常,setValue和getValue被依次执行。

setValue method threadName=threadA username=B password=BB
getValue method threadName=main username=B password=BB

小结:
1.当A线程调用anyObject的synchronized的X方法时,就取得了X方法锁,更准确的来说是anyObject的对象锁,所以其他线程必须等A线程执行完X方法才能调用X方法,但是其他线程可以所以调用非synchronized的方法。
2.当A线程调用anyObject的synchronized的X方法时,就取得了X方法锁,更准确的来说是anyObject的对象锁,所以其他线程必须等A线程执行完X方法才能调用X方法,但是如果其他线程B调用声明了synchronized的非X方法时,由于对象锁被A线程持有,必须等A线程执行完X方法后,才能执行synchronized的非X方法。这种情况不会出现脏读现象。

2.1.6锁重入

synchronized拥有锁重入功能,通俗的讲当某个线程获得对象锁以后,在此请求此对象锁可以在此得到该对象的锁。这也证明在一个synchronized方法/块内部调用本类的其他synchronized方法/块是永远可以得到锁的。
看代码:

/**
 * 锁重入
 * @author Arthur
 * @date 2017-12-26 17:25
 */

public class Run {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

class Service{

    public synchronized void service1() {
        System.out.println("service1.");
        service2();
    }

    public synchronized void service2(){
        System.out.println("service2.");
        service3();
    }

    public synchronized void service3(){
        System.out.println("service3.");
    }

}

class ThreadA extends Thread{

    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.service1();
    }
}

结果为

service1.
service2.
service3.

可重入的概念:当一个线程持有对象锁时,此时这个对象锁还未被释放,当它再次请求获得锁时还是可以获得。如果不能重入就会造成死锁。
重入锁也支持在父子类中调用。

2.1.7出现异常,锁自动释放

当一个线程执行异常时,其所持有的锁自动释放。

2.1.8同步不具有继承性

/**
 * 同步不具有继承性
 *
 * @author Arthur
 * @date 2017-12-26 17:37
 */

public class Run {
    public static void main(String[] args) {
        Sub subRef = new Sub();
        ThreadA threadA = new ThreadA(subRef);
        threadA.setName("A");
        threadA.start();
        ThreadB threadB = new ThreadB(subRef);
        threadB.setName("B");
        threadB.start();
    }
}

class Main{
    synchronized public void sleep(){
        try {
            System.out.println(" Main 下一步 sleep begin thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(" Main 下一步 sleep end thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Sub extends Main{
    @Override
    public void sleep(){
        super.sleep();
        try {
            System.out.println(" Sub 下一步 sleep begin thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(" Sub 下一步 sleep end thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{

    private Sub sub;

    public ThreadA(Sub sub) {
        this.sub = sub;
    }

    @Override
    public void run() {
        super.run();
        sub.sleep();
    }
}

class ThreadB extends Thread{

    private Sub sub;

    public ThreadB(Sub sub) {
        this.sub = sub;
    }

    @Override
    public void run() {
        super.run();
        sub.sleep();
    }
}

运行结果:

 Main 下一步 sleep begin thread name=A time=1514281689722
 Main 下一步 sleep end thread name=A time=1514281694722
 Main 下一步 sleep begin thread name=B time=1514281694722 
 Sub 下一步 sleep begin thread name=A time=1514281694722
 Main 下一步 sleep end thread name=B time=1514281699723
 Sub 下一步 sleep begin thread name=B time=1514281699723
 Sub 下一步 sleep end thread name=A time=1514281699723
 Sub 下一步 sleep end thread name=B time=1514281704723

由结果可看出同步未被继承。在子类的方法加上synchronized

public synchronized void sleep()

程序运行正常:

 Main 下一步 sleep begin thread name=B time=1514281920201
 Main 下一步 sleep end thread name=B time=1514281925201
 Sub 下一步 sleep begin thread name=B time=1514281925201
 Sub 下一步 sleep end thread name=B time=1514281930201
 Main 下一步 sleep begin thread name=A time=1514281930201
 Main 下一步 sleep end thread name=A time=1514281935202
 Sub 下一步 sleep begin thread name=A time=1514281935202
 Sub 下一步 sleep end thread name=A time=1514281940202

下一节介绍2.2 synchronized同步语句块

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

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,693评论 0 11
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,184评论 11 349
  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,629评论 0 11
  • 我的大学生活应该这么过,当我工作的时候不会感到因为曾经荒废学业而后悔,年老时不会因为浪费青春而叹息,我要把释放,天...
    TK12阅读 302评论 0 1