摘要:

  在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程訪问某一共享、可变数据时,始终都不会导致数据破坏以及其它不该出现的结果。

而全部的并发模式在解决问题时,採用的方案都是序列化訪问临界资源 。

在 Java 中,提供了两种方式来实现同步相互排斥訪问:synchronized 和 Lock。本文针对 synchronized 内置锁 具体讨论了其在 Java 并发 中的应用,包括它的具体使用场景(同步方法、同步代码块、实例对象锁 和 Class 对象锁)、可重入性 和 注意事项。


一. 线程安全问题

  在单线程中不会出现线程安全问题,而在多线程编程中。有可能会出现同一时候訪问同一个 共享、可变资源 的情况,这样的资源能够是:一个变量、一个对象、一个文件等。特别注意两点,

  • 共享: 意味着该资源能够由多个线程同一时候訪问;
  • 可变: 意味着该资源能够在其生命周期内被改动。

     所以。当多个线程同一时候訪问这样的资源的时候,就会存在一个问题:

       由于每一个线程运行的过程是不可控的。所以须要採用同步机制来协同对对象可变状态的訪问。

      

举个 数据脏读 的样例:

//资源类
class PublicVar { public String username = "A";
public String password = "AA"; //同步实例方法
public synchronized void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password; System.out.println("method=setValue " +"\t" + "threadName="
+ Thread.currentThread().getName() + "\t" + "username="
+ username + ", password=" + password);
} catch (InterruptedException e) {
e.printStackTrace();
}
} //非同步实例方法
public void getValue() {
System.out.println("method=getValue " + "\t" + "threadName="
+ Thread.currentThread().getName()+ "\t" + " username=" + username
+ ", password=" + password);
}
} //线程类
class ThreadA extends Thread { private PublicVar publicVar; public ThreadA(PublicVar publicVar) {
super();
this.publicVar = publicVar;
} @Override
public void run() {
super.run();
publicVar.setValue("B", "BB");
}
} //測试类
public class Test { public static void main(String[] args) {
try {
//临界资源
PublicVar publicVarRef = new PublicVar(); //创建并启动线程
ThreadA thread = new ThreadA(publicVarRef);
thread.start(); Thread.sleep(200);// 打印结果受此值大小影响 //在主线程中调用
publicVarRef.getValue(); } catch (InterruptedException e) {
e.printStackTrace();
}
}
}/* Output ( 数据交叉 ):
method=getValue threadName=main username=B, password=AA
method=setValue threadName=Thread-0 username=B, password=BB
*///:~

  由程序输出可知,尽管在写操作进行了同步,但在读操作上仍然有可能出现一些意想不到的情况。比如上面所看到的的 脏读。发生 脏读 的情况是在运行读操作时,对应的数据已被其它线程 部分改动 过。导致 数据交叉 的现象产生。

  这事实上就是一个线程安全问题,即多个线程同一时候訪问一个资源时。会导致程序运行结果并非想看到的结果。这里面,这个资源被称为:临界资源。也就是说,当多个线程同一时候訪问临界资源(一个对象,对象中的属性。一个文件。一个数据库等)时。就可能会产生线程安全问题。

  只是,当多个线程运行一个方法时,该方法内部的局部变量并非临界资源,由于这些局部变量是在每一个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。


二. 怎样解决线程安全问题

  实际上,全部的并发模式在解决线程安全问题时,採用的方案都是 序列化訪问临界资源

即在同一时刻,仅仅能有一个线程訪问临界资源,也称作 同步相互排斥訪问

换句话说,就是在訪问临界资源的代码前面加上一个锁。当訪问完临界资源后释放锁,让其它线程继续訪问。

  在 Java 中,提供了两种方式来实现同步相互排斥訪问:synchronized 和 Lock。本文主要讲述 synchronized 的用法。Lock 的用法我的还有一篇博文《Java 并发:Lock 框架具体解释》中阐述。


三. synchronized 同步方法或者同步块

  在了解 synchronized 关键字的用法之前。我们先来看一个概念:相互排斥锁。即 能到达到相互排斥訪问目的的锁。举个简单的样例,假设对临界资源加上相互排斥锁,当一个线程在訪问该临界资源时。其它线程便仅仅能等待。

  在 Java 中。能够使用 synchronized 关键字来标记一个方法或者代码块。当某个线程调用该对象的synchronized方法或者訪问synchronized代码块时,这个线程便获得了该对象的锁,其它线程临时无法訪问这种方法,仅仅有等待这种方法运行完成或者代码块运行完成。这个线程才会释放该对象的锁,其它线程才干运行这种方法或者代码块。


  以下这段代码中两个线程分别调用insertData对象插入数据:

1) synchronized方法

public class Test {

    public static void main(String[] args)  {
final InsertData insertData = new InsertData();
// 启动线程 1
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start(); // 启动线程 2
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
} class InsertData { // 共享、可变资源
private ArrayList<Integer> arrayList = new ArrayList<Integer>(); //对共享可变资源的訪问
public void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}/* Output:
Thread-0在插入数据0
Thread-1在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-1在插入数据1
Thread-1在插入数据2
*///:~

  依据运行结果就能够看出,这两个线程在同一时候运行insert()方法。而假设在insert()方法前面加上关键字synchronized 的话,运行结果为:

class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public synchronized void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}/* Output:
Thread-0在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
*///:~

  从以上输出结果能够看出。Thread-1 插入数据是等 Thread-0 插入完数据之后才进行的。

说明 Thread-0 和 Thread-1 是顺序运行 insert() 方法的。

这就是 synchronized 关键字对方法的作用。

  只是须要注意以下三点:

  1)当一个线程正在訪问一个对象的 synchronized 方法,那么其它线程不能訪问该对象的其它 synchronized 方法。这个原因非常easy,由于一个对象仅仅有一把锁。当一个线程获取了该对象的锁之后。其它线程无法获取该对象的锁,所以无法訪问该对象的其它synchronized方法。

  2)当一个线程正在訪问一个对象的 synchronized 方法,那么其它线程能訪问该对象的非 synchronized 方法。这个原因非常easy,訪问非 synchronized 方法不须要获得该对象的锁,假如一个方法没用 synchronized 关键字修饰,说明它不会使用到临界资源,那么其它线程是能够訪问这种方法的,

  3)假设一个线程 A 须要訪问对象 object1 的 synchronized 方法 fun1,另外一个线程 B 须要訪问对象 object2 的 synchronized 方法 fun1。即使 object1 和 object2 是同一类型),也不会产生线程安全问题,由于他们訪问的是不同的对象。所以不存在相互排斥问题。


2) synchronized 同步块

  synchronized 代码块相似于以下这样的形式:

synchronized (lock){
//訪问共享可变资源
...
}

  当在某个线程中运行这段代码块。该线程会获取对象lock的锁,从而使得其它线程无法同一时候訪问该代码块。当中,lock 能够是 this。代表获取当前对象的锁,也能够是类中的一个属性。代表获取该属性的锁。

特别地, 实例同步方法synchronized(this)同步块 是相互排斥的,由于它们锁的是同一个对象。但与 synchronized(非this)同步块 是异步的。由于它们锁的是不同对象。

  比方上面的insert()方法能够改成以下两种形式:

// this 监视器
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public void insert(Thread thread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
} // 对象监视器
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object(); public void insert(Thread thread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}

  从上面代码能够看出。synchronized代码块 比 synchronized方法 的粒度更细一些,使用起来也灵活得多。

由于或许一个方法中仅仅有一部分代码仅仅须要同步,假设此时对整个方法用synchronized进行同步,会影响程序运行效率。

而使用synchronized代码块就能够避免这个问题。synchronized代码块能够实现仅仅对须要同步的地方进行同步。


3) class 对象锁

  特别地。每一个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外。它能够用来控制对 static 数据成员 (static 数据成员不专属于不论什么一个对象,是类成员) 的并发訪问。

而且,假设一个线程运行一个对象的非static synchronized 方法,另外一个线程须要运行这个对象所属类的 static synchronized 方法,也不会发生相互排斥现象。

由于訪问 static synchronized 方法占用的是类锁,而訪问非 static synchronized 方法占用的是对象锁,所以不存在相互排斥现象。比如,

public class Test {

    public static void main(String[] args)  {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
} class InsertData { // 非 static synchronized 方法
public synchronized void insert(){
System.out.println("运行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("运行insert完成");
} // static synchronized 方法
public synchronized static void insert1() {
System.out.println("运行insert1");
System.out.println("运行insert1完成");
}
}/* Output:
运行insert
运行insert1
运行insert1完成
运行insert完成
*///:~

  依据运行结果。我们能够看到第一个线程里面运行的是insert方法。不会导致第二个线程运行insert1方法发生堵塞现象。以下,我们看一下 synchronized 关键字究竟做了什么事情。我们来反编译它的字节码看一下,以下这段代码反编译后的字节码为:

public class InsertData {
private Object object = new Object(); public void insert(Thread thread){
synchronized (object) {}
} public synchronized void insert1(Thread thread){} public void insert2(Thread thread){}
}

            

           

  从反编译获得的字节码能够看出。synchronized 代码块实际上多了 monitorenter 和 monitorexit 两条指令。 monitorenter指令运行时会让对象的锁计数加1。而monitorexit指令运行时会让对象的锁计数减1。事实上这个与操作系统里面的PV操作非常像,操作系统里面的PV操作就是用来控制多个进程对临界资源的訪问。

对于synchronized方法。运行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自己主动获取对象的锁。调用方法。最后释放锁。假设有异常发生,线程自己主动释放锁。

  有一点要注意:对于 synchronized方法 或者 synchronized代码块。当出现异常时,JVM会自己主动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

  


四. 可重入性

  一般地,当某个线程请求一个由其它线程持有的锁时,发出请求的线程就会堵塞。然而,由于 Java 的内置锁是可重入的,因此假设某个线程试图获得一个已经由它自己持有的锁时,那么这个请求就会成功。可重入锁最大的作用是避免死锁。

比如:

public class Test implements Runnable {

    // 可重入锁測试
public synchronized void get() {
System.out.println(Thread.currentThread().getName());
set();
} public synchronized void set() {
System.out.println(Thread.currentThread().getName());
} @Override
public void run() {
get();
} public static void main(String[] args) {
Test test = new Test();
new Thread(test,"Thread-0").start();
new Thread(test,"Thread-1").start();
new Thread(test,"Thread-2").start();
}
}/* Output:
Thread-1
Thread-1
Thread-2
Thread-2
Thread-0
Thread-0
*///:~

五. 注意事项

1). 内置锁与字符串常量

  由于字符串常量池的原因,在大多数情况下,同步synchronized代码块 都不使用 String 作为锁对象,而改用其它,比方 new Object() 实例化一个 Object 对象,由于它并不会被放入缓存中。

看以下的样例:

//资源类
class Service {
public void print(String stringParam) {
try {
synchronized (stringParam) {
while (true) {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} //线程A
class ThreadA extends Thread {
private Service service; public ThreadA(Service service) {
super();
this.service = service;
} @Override
public void run() {
service.print("AA");
}
} //线程B
class ThreadB extends Thread {
private Service service; public ThreadB(Service service) {
super();
this.service = service;
} @Override
public void run() {
service.print("AA");
}
} //測试
public class Run {
public static void main(String[] args) { //临界资源
Service service = new Service(); //创建并启动线程A
ThreadA a = new ThreadA(service);
a.setName("A");
a.start(); //创建并启动线程B
ThreadB b = new ThreadB(service);
b.setName("B");
b.start(); }
}/* Output (死锁):
A
A
A
A
...
*///:~

  出现上述结果就是由于 String 类型的參数都是 “AA”,两个线程持有同样的锁,所以 线程B 始终得不到运行。造成死锁。

进一步地,所谓死锁是指:

    不同的线程都在等待根本不可能被释放的锁。从而导致全部的任务都无法继续完成。


b). 锁的是对象而非引用

  在将不论什么数据类型作为同步锁时,须要注意的是。是否有多个线程将同一时候去竞争该锁对象:

  1).若它们将同一时候竞争同一把锁,则这些线程之间就是同步的;

  2).否则。这些线程之间就是异步的。

看以下的样例:

//资源类
class MyService {
private String lock = "123"; public void testMethod() {
try {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " begin "
+ System.currentTimeMillis());
lock = "456";
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " end "
+ System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} //线程B
class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) {
super();
this.service = service;
} @Override
public void run() {
service.testMethod();
}
} //线程A
class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) {
super();
this.service = service;
} @Override
public void run() {
service.testMethod();
}
} //測试
public class Run1 {
public static void main(String[] args) throws InterruptedException { //临界资源
MyService service = new MyService(); //线程A
ThreadA a = new ThreadA(service);
a.setName("A"); //线程B
ThreadB b = new ThreadB(service);
b.setName("B"); a.start();
Thread.sleep(50);// 存在50毫秒
b.start();
}
}/* Output(循环):
A begin 1484319778766
B begin 1484319778815
A end 1484319780766
B end 1484319780815
*///:~

  由上述结果可知,线程 A、B 是异步的。由于50毫秒过后, 线程B 取得的锁对象是 “456”,而 线程A 依旧持有的锁对象是 “123”。

所以,这两个线程是异步的。若将上述语句 “Thread.sleep(50);” 凝视。则有:

//測试
public class Run1 {
public static void main(String[] args) throws InterruptedException { //临界资源
MyService service = new MyService(); //线程A
ThreadA a = new ThreadA(service);
a.setName("A"); //线程B
ThreadB b = new ThreadB(service);
b.setName("B"); a.start();
// Thread.sleep(50);// 存在50毫秒
b.start();
}
}/* Output(循环):
B begin 1484319952017
B end 1484319954018
A begin 1484319954018
A end 1484319956019
*///:~

  由上述结果可知,线程 A、B 是同步的。由于线程 A、B 竞争的是同一个锁“123”,尽管先获得运行的线程将 lock 指向了 对象“456”。但结果还是同步的。由于线程 A 和 B 共同争抢的锁对象是“123”,也就是说。锁的是对象而非引用。


六. 总结

  用一句话来说。synchronized 内置锁 是一种 对象锁 (锁的是对象而非引用), 作用粒度是对象 ,能够用来实现对 临界资源的同步相互排斥訪问 ,是 可重入 的。特别地。对于 临界资源 有:

  • 若该资源是静态的,即被 static 关键字修饰。那么訪问它的方法必须是同步且是静态的,synchronized 块必须是 class锁;

  • 若该资源是非静态的,即没有被 static 关键字修饰,那么訪问它的方法必须是同步的,synchronized 块是实例对象锁;

      

实质上。关键字synchronized 主要包括两个特征:

  • 相互排斥性:保证在同一时刻,仅仅有一个线程能够运行某一个方法或某一个代码块。

  • 可见性:保证线程工作内存中的变量与公共内存中的变量同步。使多线程读取共享变量时能够获得最新值的使用。


引用

《Java 并发编程实战》

《Java 多线程编程核心技术》

Java并发编程:synchronized

Java 并发:内置锁 Synchronized的更多相关文章

  1. java 并发——内置锁

    坚持学习,总会有一些不一样的东西. 一.由单例模式引入 引用一下百度百科的定义-- 线程安全是多线程编程时的计算机程序代码中的一个概念.在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同 ...

  2. JAVA并发-内置锁和ThreadLocal

    上一篇博客讲过,当多个线程访问共享的可变变量的时候,可以使用锁来进行线程同步.那么如果线程安全性存在的3个前提条件不同时存在的话,自然就不需要考虑线程安全性了.或者说如果我们能够将某个共享变量变为局部 ...

  3. 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢? ...

  4. jvm内置锁synchronized不能被中断

    很久没看技术书籍了,今天看了一下<七周七并发模型>前面两章讲的java,写的还是有深度的.看到了一个有demo,说jvm内置锁synchronized是不能被中断的.照着书上写了个demo ...

  5. Java内置锁synchronized的实现原理及应用(三)

    简述 Java中每个对象都可以用来实现一个同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock). 具体表现形式如下: 1.普通同步方法,锁的是当前实例对象 ...

  6. Java内置锁synchronized的实现原理

    简述Java中每个对象都可以用来实现一个同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock). 具体表现形式如下: 1.普通同步方法,锁的是当前实例对象 ...

  7. Java内置锁synchronized的可重入性

    学习自 https://blog.csdn.net/aigoogle/article/details/29893667 对我很有帮助 感谢作者

  8. 深入理解Java内置锁和显式锁

    synchronized and Reentrantlock 多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两 ...

  9. Java内置锁和简单用法

    一.简单的锁知识 关于内置锁 Java具有通过synchronized关键字实现的内置锁,内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁. jav ...

随机推荐

  1. SOJ 4552 [期望,概率]

    题目链接:[http://acm.scu.edu.cn/soj/problem.action?id=4552] 题意:给你n种卡牌,每种卡牌有无限多个,每次从中抽取一张卡牌,问:1.集齐这n种卡牌需要 ...

  2. BZOJ1007 水平相交直线

    按照斜率排序,我们可以想象如果你能看到大于等于三条直线那么他一定会组成一个下凸包,这样我们只需要判断如果当前这条直线与栈顶第二直线相交点相比于栈顶第二直线与栈顶直线相交点靠左那么他就不满足凸包性质. ...

  3. [BZOJ3926][ZJOI2015]诸神眷顾的幻想乡(后缀自动机)

    日,无数幽香的粉丝到了幽香家门前的太阳花田上来为幽香庆祝生日. 粉丝们非常热情,自发组织表演了一系列节目给幽香看.幽香当然也非常高兴啦.  这时幽香发现了一件非常有趣的事情,太阳花田有n块空地.在过去 ...

  4. [BZOJ3583]杰杰的女性朋友(矩阵快速幂)

    杰杰的女性朋友 时间限制:10s      空间限制:256MB 题目描述 杰杰是魔法界的一名传奇人物.他对魔法具有深刻的洞察力,惊人的领悟力,以及令人叹为观止的创造力.自从他从事魔法竞赛以来,短短几 ...

  5. [转]Android Studio开发入门-引用jar及so文件

    注意: 1.jar包在app的libs目录 2.so文件放在src/main”目录中名为“jniLibs”的目录 一.引用jar文件    1.将jar文件复制.粘贴到app的libs目录中:    ...

  6. [转]Android Service完全解析,关于服务你所需知道的一切

      目录(?)[+] Android Service完全解析,关于服务你所需知道的一切(上) 分类: Android疑难解析2013-10-31 08:10 6451人阅读 评论(39) 收藏 举报 ...

  7. 【原创】Eclipse导入Android项目报错解决

    1.点击报错的项目--->右键--->Properties--->选择Android--->将Project Build Target选择其一勾上-->Is Librar ...

  8. 【原】Eclipse部署Maven web项目到tomcat服务器时,没有将lib下的jar复制过去的解决办法

    我们在做web开发是,经常都要在eclipse中搭建web服务器,并将开发中的web项目部署到web服务器进行调试,在此,我选择的是tomcat服务器.之前部署web项目到tomcat进行启动调试都很 ...

  9. 使用apache htpasswd生成加密的password文件,并使用.htaccess控制文件夹訪问

    htpasswd 是apache的小工具.在apache安装文件夹bin下可找到. Usage: htpasswd [-cmdpsD] passwordfile username htpasswd - ...

  10. 我的sourceinsight的配置

    下面是我的sourceinsight的配置,点击下面的链接,下载*.em文件,将他们添加到Base工程,设置相应的快捷键即可,或者导入下载的配置文件. http://pan.baidu.com/s/1 ...