synchronized

方法内变量是线程安全

关于“线程安全”和“非线程安全”相关的技术点,是学习多线程一定会遇到的经典问题。“非线程安全”通常发生在多个线程对同一个对象的实例变量进行并发访问。产生的后果就是“脏读”,也就是读取到的数据其实是被其他线程更改过的。synchronized 通过同步互斥来解决这个问题。

注意,“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量。那么不存在“非线程安全”问题。所得到的结果自然就是“线程安全”的。

package com.skystep.thread.threadsafe;

public class HasSelfPrivateNum {

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

    }

}

package com.skystep.thread.threadsafe;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}
package com.skystep.thread.threadsafe;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.threadsafe;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

其中 num 为方法内变量,两个线程如期望输出结果。可见,方法中的变量不存在非线程安全问题,永远是线程安全的,因为方法内部的变量是私有造成的。

实例变量是非线程安全

如果多个线程并发访问同一个对象的某个实例变量,便可能发生“非线程安全”问题,线程访问的对象如果有多个实例变量,则运行结果有可能出现交叉的情况。如果对象只有一个实例变量,则有可能出现覆盖的情况。

package com.skystep.thread.threadunsafe;

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

    }

}

package com.skystep.thread.threadunsafe;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}
package com.skystep.thread.threadunsafe;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.threadunsafe;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=200

两个线程同时访问对象中实例变量。便出现了“非线程安全问题”。对于此类问题,早在 JAVA 1.6时便提供了 synchronized 同步互斥锁来解决此问题。

synchronized 同步方法

在 Java 语言中,每一个对象都会内置一把锁(监视器锁),这是一种互斥锁,每一时间片只能有一个线程获取该锁,其他线程进入等待,直到原先线程释放锁。 线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。

package com.skystep.thread.sync;

public class HasSelfPrivateNum {
    private int num = 0;

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

    }

}
package com.skystep.thread.sync;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

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


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

package com.skystep.thread.sync;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.sync;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
a num=100
b set over
b num=200

通过对 addI 方法添加 synchronized 关键字修饰之后,该程序重新符合预期输出结果。当 ThreadA 线程获取到 hasSelfPrivateNum 对象锁时,ThreadB 会进入等待,直到 ThreadA 执行完 addI 方法。这就是同步互斥。

synchronized 错误使用 多个对象多个锁

从前面例子来看,关键字 synchronized 取得的锁都是对象锁,而不是一段代码或者函数,不同线程必须竞争同一把锁,才能达到同步互斥的目的。但是对于初学者常常会出现使用错误。出现多个对象多把锁的问题。

package com.skystep.thread.syncerroruse;

public class HasSelfPrivateNum {
    private int num = 0;

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

    }

}
package com.skystep.thread.syncerroruse;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

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


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

package com.skystep.thread.syncerroruse;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

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


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.syncerroruse;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNumA);
        a.start();
        HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
        ThreadB b = new ThreadB(hasSelfPrivateNumB);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

从例子来看,main 线程创建了多个线程。ThreadA 线程使用的是 hasSelfPrivateNumA 对象作为锁,ThreadB 使用 hasSelfPrivateNumB 对象锁,并不能达到同步互斥的目标。

synchronized 方法和锁对象 锁住的是谁

使用 synchronized 修饰函数,便是将函数调用者实例作为对象锁。不同线程调用被 synchronized 修饰的函数,必须抢占该对象锁,谁先抢到谁先执行函数代码,抢不到的线程进入阻塞状态,直到锁被释放。

package com.skystep.thread.syncerroruse;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNumA);
        a.start();
        HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
        ThreadB b = new ThreadB(hasSelfPrivateNumB);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。

synchronized 锁重入 无限套娃不违法

关键字 synchronized 拥有重入锁的功能 。当一个线程获取到对象锁时,再次请求此对象锁时是可以再次得到该对象的锁的。JAVA 中称为锁重入。是同一锁,才有重入一说,套的娃必须是同一个。

重入锁的概念是:自己可以再次获取自己的内部锁,比如线程 A 获得某个对象的锁,此时锁没有释放,当其再次获取这个对象的锁的时候可以获得。如果不可的重入的锁,就会造成死锁。

synchronized 锁释放 遇异常自动释放

ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。

synchronized 同步代码块

被 synchronized 关键字修饰的语句块会自动被加上内置锁,从而实现同步。

public class Counter {
    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add() {
    	synchronized (this){
    		this.num++;
		}
    }
}
public class Sync {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.setNum(0);

        Thread[] threds = new Thread[2];
        for (int i = 0; i < 2; i++) {
            threds[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 2000; j++) {
                        counter.add();
                    }
                }
            });
        }

        threds[0].start();
        threds[1].start();

        try {
            threds[0].join();
            threds[1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("num:" + counter.getNum());

    }
}

执行结果:
num:4000

在代码段 num++ 中添加 synchronized 修饰,便完成共有变量 num 的读写同步。

简述 synchronized 实现原理

在多线程并发编程中 synchronized 一直是元老级的存在。很多人称之为重量级锁,但在 JAVA SE1.6 进行了各种优化之后,已经没有那么重量级了。为了减少获取锁和释放锁带来的消耗而引进了偏向锁和轻量级锁。

JAVA 中的每一个对象都可以作为锁,具体表象有如下三种:

  • 对于普通同步方法,锁是当前的实例对象;
  • 对于静态同步方法,锁是当前类的 Class 对象;
  • 对于同步方法快,锁的是括号里配置的对象;

在 JVM 规范中可以知道,JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。monitorenter 指令在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处或者异常的地方,JVM 保证了每个 monitorenter 都有与之对应的 monitorexit。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有之后,它将处于锁定状态。线程执行到 monitorexit 指令后,将会尝试获取对象对应的 monitor 的所有权,即尝试获得对象的锁。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值