`
jieke_ZJ
  • 浏览: 44849 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

深入解析Java并发程序中线程的同步与线程锁的使用

 
阅读更多

synchronized关键字

synchronized,我们谓之锁,主要用来给方法、代码块加锁。当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。

synchronized主要包括两种方法:synchronized 方法、synchronized 块。

synchronized 方法

通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:

public synchronized void getResult(); 
synchronized方法控制对类成员变量的访问。它是如何来避免类成员变量的访问控制呢?我们知道方法使用了synchronized关键字表明该方法已加锁,在任一线程在访问改方法时都必须要判断该方法是否有其他线程在“独占”。每个类实例对应一个把锁,每个synchronized方法都必须调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,被阻塞的线程方能获得该锁。

其实synchronized方法是存在缺陷的,如果我们将一个很大的方法声明为synchronized将会大大影响效率的。如果多个线程在访问一个synchronized方法,那么同一时刻只有一个线程在执行该方法,而其他线程都必须等待,但是如果该方法没有使用synchronized,则所有线程可以在同一时刻执行它,减少了执行的总时间。所以如果我们知道一个方法不会被多个线程执行到或者说不存在资源共享的问题,则不需要使用synchronized关键字。但是如果一定要使用synchronized关键字,那么我们可以synchronized代码块来替换synchronized方法。

synchronized 块

synchronized代码块所起到的作用和synchronized方法一样,只不过它使临界区变的尽可能短了,换句话说:它只把需要的共享数据保护起来,其余的长代码块留出此操作。语法如下:

1
2
3
4
5
6
7
8
synchronized(object) {
 //允许访问控制的代码
}
如果我们需要以这种方式来使用synchronized关键字,那么必须要通过一个对象引用来作为参数,通常这个参数我们常使用为this.
 
synchronized (this) {
 //允许访问控制的代码
}

对于synchronized(this)有如下理解:

1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问object中的非synchronized(this)同步代码块。

3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其他synchronized(this)同步代码块得访问将被阻塞。

4、第三个例子同样适用其他同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其他线程对该object对象所有同步代码部分的访问都将被暂时阻塞。


在java多线程中存在一个“先来后到”的原则,也就是说谁先抢到钥匙,谁先用。我们知道为避免资源竞争产生问题,java使用同步机制来避免,而同步机制是使用锁概念来控制的。那么在Java程序当中,锁是如何体现的呢?这里我们需要弄清楚两个概念:

什么是锁?在日常生活中,它就是一个加在门、箱子、抽屉等物体上的封缄器,防止别人偷窥或者偷盗,起到一个保护的作用。在java中同样如此,锁对对象起到一个保护的作用,一个线程如果独占了某个资源,那么其他的线程别想用,想用?等我用完再说吧!

在java程序运行环境中,JVM需要对两类线程共享的数据进行协调:

1、保存在堆中的实例变量

2、保存在方法区中的类变量。

在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象来说,相关联的监视器保护对象的实例变量。 对于类来说,监视器保护类的类变量。如果一个对象没有实例变量,或者说一个类没有变量,相关联的监视器就什么也不监视。

为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。 如果某个线程获取了锁,那么在它释放该锁之前其他线程是不可能获取同样锁的。一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。
java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。

一个简单的锁

在使用synchronized时,我们是这样使用锁的:

1
2
3
4
5
6
7
public class ThreadTest {
 public void test(){
  synchronized(this){
   //do something
  }
 }
}

synchronized可以确保在同一时间内只有一个线程在执行dosomething。下面是使用lock替代synchronized:

1
2
3
4
5
6
7
8
public class ThreadTest {
 Lock lock = new Lock();
 public void test(){
  lock.lock();
  //do something
  lock.unlock();
 }
}

lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。


锁的是什么?

在这个问题之前我们必须要明确一点:无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象。在java中每一个对象都可以作为锁,它主要体现在下面三个方面:

对于同步方法,锁是当前实例对象。
对于同步方法块,锁是Synchonized括号里配置的对象。


对于静态同步方法,锁是当前对象的Class对象。
首先我们先看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadTest_01 implements Runnable{
  
 @Override
 public synchronized void run() {
  for(int i = 0 ; i < 3 ; i++){
   System.out.println(Thread.currentThread().getName() + "run......");
  }
 }
   
 public static void main(String[] args) {
  for(int i = 0 ; i < 5 ; i++){
   new Thread(new ThreadTest_01(),"Thread_" + i).start();
  }
 }
}

部分运行结果:

1
2
3
4
5
6
7
8
9
Thread_2run......
Thread_2run......
Thread_4run......
Thread_4run......
Thread_3run......
Thread_3run......
Thread_3run......
Thread_2run......
Thread_4run......

这个结果与我们预期的结果有点不同(这些线程在这里乱跑),照理来说,run方法加上synchronized关键字后,会产生同步效果,这些线程应该是一个接着一个执行run方法的。在上面LZ提到,一个成员方法加上synchronized关键字后,实际上就是给这个成员方法加上锁,具体点就是以这个成员方法所在的对象本身作为对象锁。但是在这个实例当中我们一共new了10个ThreadTest对象,那个每个线程都会持有自己线程对象的对象锁,这必定不能产生同步的效果。所以:如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的!

这个时候synchronized锁住的是那个对象?它锁住的就是调用这个同步方法对象。就是说threadTest这个对象在不同线程中执行同步方法,就会形成互斥。达到同步的效果。所以将上面的new Thread(new ThreadTest_01(),”Thread_” + i).start(); 修改为new Thread(threadTest,”Thread_” + i).start();就可以了。

对于同步方法,锁是当前实例对象。

上面实例是使用synchronized方法,我们在看看synchronized代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ThreadTest_02 extends Thread{
  
 private String lock ;
 private String name;
   
 public ThreadTest_02(String name,String lock){
  this.name = name;
  this.lock = lock;
 }
   
 @Override
 public void run() {
  synchronized (lock) {
   for(int i = 0 ; i < 3 ; i++){
    System.out.println(name + " run......");
   }
  }
 }
   
 public static void main(String[] args) {
  String lock = new String("test");
  for(int i = 0 ; i < 5 ; i++){
   new ThreadTest_02("ThreadTest_" + i,lock).start();
  }
 }
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ThreadTest_0 run......
ThreadTest_0 run......
ThreadTest_0 run......
ThreadTest_1 run......
ThreadTest_1 run......
ThreadTest_1 run......
ThreadTest_4 run......
ThreadTest_4 run......
ThreadTest_4 run......
ThreadTest_3 run......
ThreadTest_3 run......
ThreadTest_3 run......
ThreadTest_2 run......
ThreadTest_2 run......
ThreadTest_2 run......

在main方法中我们创建了一个String对象lock,并将这个对象赋予每一个ThreadTest2线程对象的私有变量lock。我们知道java中存在一个字符串池,那么这些线程的lock私有变量实际上指向的是堆内存中的同一个区域,即存放main函数中的lock变量的区域,所以对象锁是唯一且共享的。线程同步!!

在这里synchronized锁住的就是lock这个String对象。

  对于同步方法块,锁是Synchonized括号里配置的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTest_03 extends Thread{
  
 public synchronized static void test(){
  for(int i = 0 ; i < 3 ; i++){
   System.out.println(Thread.currentThread().getName() + " run......");
  }
 }
   
 @Override
 public void run() {
  test();
 }
  
 public static void main(String[] args) {
  for(int i = 0 ; i < 5 ; i++){
   new ThreadTest_03().start();
  }
 }
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread-0 run......
Thread-0 run......
Thread-0 run......
Thread-4 run......
Thread-4 run......
Thread-4 run......
Thread-1 run......
Thread-1 run......
Thread-1 run......
Thread-2 run......
Thread-2 run......
Thread-2 run......
Thread-3 run......
Thread-3 run......
Thread-3 run......

在这个实例中,run方法使用的是一个同步方法,而且是static的同步方法,那么这里synchronized锁的又是什么呢?我们知道static超脱于对象之外,它属于类级别的。所以,对象锁就是该静态放发所在的类的Class实例。由于在JVM中,所有被加载的类都有唯一的类对象,在该实例当中就是唯一的 ThreadTest_03.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!所以对象锁是唯一且共享的。线程同步!!

对于静态同步方法,锁是当前对象的Class对象。

如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized的instance函数B,那么这个类的同一对象Obj,在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

锁的升级

java中锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。下面主要部分主要是对博客:聊聊并发(二)Java SE1.6中的Synchronized的总结。

锁自旋

我们知道在当某个线程在进入同步方法/代码块时若发现该同步方法/代码块被其他现在所占,则它就要等待,进入阻塞状态,这个过程性能是低下的。

在遇到锁的争用或许等待事,线程可以不那么着急进入阻塞状态,而是等一等,看看锁是不是马上就释放了,这就是锁自旋。锁自旋在一定程度上可以对线程进行优化处理。

偏向锁

偏向锁主要为了解决在没有竞争情况下锁的性能问题。在大多数情况下锁锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当某个线程获得锁的情况,该线程是可以多次锁住该对象,但是每次执行这样的操作都会因为CAS(CPU的Compare-And-Swap指令)操作而造成一些开销消耗性能,为了减少这种开销,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

当有其他线程在尝试着竞争偏向锁时,持有偏向锁的线程就会释放锁。

锁膨胀

多个或多次调用粒度太小的锁,进行加锁解锁的消耗,反而还不如一次大粒度的锁调用来得高效。

轻量级锁

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。轻量级锁在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的指向和状态。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

锁的公平性

公平性的对立面是饥饿。那么什么是“饥饿”呢?如果一个线程因为其他线程在一直抢占着CPU而得不到CPU运行时间,那么我们就称该线程被“饥饿致死”。而解决饥饿的方案则被称之为“公平性”——所有线程均可以公平地获得CPU运行机会。

导致线程饥饿主要有如下几个原因:

高优先级线程吞噬所有的低优先级线程的CPU时间。我们可以为每个线程单独设置其优先级,从1到10。优先级越高的线程获得CPU的时间越多。对大多数应用来说,我们最好是不要改变其优先级值。

线程被永久堵塞在一个等待进入同步块的状态。java的同步代码区是导致线程饥饿的重要因素。java的同步代码块并不会保证进入它的线程的先后顺序。这就意味着理论上存在一个或者多个线程在试图进入同步代码区时永远被堵塞着,因为其他线程总是不断优于他获得访问权,导致它一直得到不到CPU运行机会被“饥饿致死”。

线程在等待一个本身也处于永久等待完成的对象。如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

为了解决线程“饥饿”的问题,我们可以使用锁实现公平性。

锁的可重入性

我们知道当线程请求一个由其它线程持有锁的对象时,该线程会阻塞,但是当线程请求由自己持有锁的对象时,是否可以成功呢?答案是可以成功的,成功的保障就是线程锁的“可重入性”。

“可重入”意味着自己可以再次获得自己的内部锁,而不需要阻塞。如下:

1
2
3
4
5
6
7
8
9
10
11
public class Father {
 public synchronized void method(){
  //do something
 }
}
public class Child extends Father{
 public synchronized void method(){
  //do something
  super.method();
 }
}

 
如果所是不可重入的,上面的代码就会死锁,因为调用child的method(),首先会获取父类Father的内置锁然后获取Child的内置锁,当调用父类的方法时,需要再次后去父类的内置锁,如果不可重入,可能会陷入死锁。

java多线程的可重入性的实现是通过每个锁关联一个请求计算和一个占有它的线程,当计数为0时,认为该锁是没有被占有的,那么任何线程都可以获得该锁的占有权。当某一个线程请求成功后,JVM会记录该锁的持有线程 并且将计数设置为1,如果这时其他线程请求该锁时则必须等待。当该线程再次请求请求获得锁时,计数会+1;当占有线程退出同步代码块时,计数就会-1,直到为0时,释放该锁。这时其他线程才有机会获得该锁的占有权。

lock及其实现类

java.util.concurrent.locks提供了非常灵活锁机制,为锁定和等待条件提供一个框架的接口和类,它不同于内置同步和监视器,该框架允许更灵活地使用锁定和条件。它的类结构图如下:

20163285704787.jpg (783×548)

ReentrantLock:一个可重入的互斥锁,为lock接口的主要实现。

ReentrantReadWriteLock:

ReadWriteLock:ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。

Semaphore:一个计数信号量。

Condition:锁的关联条件,目的是允许线程获取锁并且查看等待的某一个条件是否满足。

CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。

分享到:
评论

相关推荐

    java线程深入解析

    本文将深入解析Java线程的相关知识点,包括线程的定义、创建、状态管理、线程同步和安全问题。 1. **线程定义** 在Java中,线程是进程中的单一顺序控制流,是程序执行的基本单元。线程在进程的上下文中运行,共享...

    java高并发程序设计(原版电子书)

    4. **并发集合**:深入分析在并发环境中使用的特殊集合,如`ConcurrentHashMap`、`CopyOnWriteArrayList`和`ConcurrentLinkedQueue`,它们为并发操作提供了线程安全的解决方案。 5. **并发工具类**:介绍`...

    实战Java高并发程序设计-试读

    最后,书中可能介绍了一些最佳实践,如如何避免并发问题、如何正确使用线程同步机制,以及如何进行性能优化等,这些都是实际开发中非常实用的知识。 通过阅读这本书,你可以对Java并发编程有更深入的理解,掌握必要...

    Java 并发核心编程

    在深入讨论具体技术细节之前,我们先了解一些关键的Java并发概念: 1. **Java Memory Model (JMM)**: - **定义**: 在Java SE 5 (JSR133)中引入的Java Memory Model (JMM)旨在确保编写并发代码时的一致性和可预测性...

    深入解析Java多线程

    本文将深入解析Java多线程,帮助你快速掌握这一关键技能。 首先,我们要明白什么是线程。线程是操作系统调度的基本单位,每个线程都有自己的程序计数器、栈、局部变量和状态,它们共享同一块内存空间,即进程的内存...

    《java 并发编程实战高清PDF版》

    锁是Java并发编程中用于同步的关键工具。书中深入剖析了各种锁机制,如内置锁(也称为监视器锁),通过`synchronized`关键字实现。此外,还介绍了高级的锁接口`java.util.concurrent.locks`,如`ReentrantLock`,它...

    Java多线程同步具体实例讲解 .doc

    本文将通过一个具体的售票系统实例来深入解析Java中的线程同步。 首先,我们要了解Java中的关键字`synchronized`,它是实现线程同步的主要手段。当一个方法或代码块被`synchronized`修饰时,同一时间只有一个线程...

    线程同步面试题深入解析

    线程同步是多线程编程中的关键概念,...总的来说,线程同步是Java多线程编程中不可或缺的一部分,正确理解和使用同步机制能确保程序在并发环境下的正确性和高效性。在面试中,理解并能熟练应用这些知识点是非常重要的。

    Java系列深入解析Java多线程

    本文将深入探讨Java多线程的各个方面,包括基础知识、创建线程、线程同步与通信、死锁问题以及线程池。 1. **基础知识** - **线程与进程**:线程是操作系统分配CPU时间的基本单位,而进程是系统中运行的程序实例。...

    java并发实战中文文档

    文档会深入解析`synchronized`的使用,包括同步代码块和同步方法,并讨论其与监视器对象的关系。 3. **锁**:Java并发库中的`java.util.concurrent.locks`包提供了更高级的锁机制,如可重入锁(`ReentrantLock`)、...

    一本经典的多线程书籍 Java并发编程 设计原则与模式 第二版 (英文原版)

    书中深入讨论了Java并发容器,如ArrayList、LinkedList、HashSet、HashMap以及并发优化过的ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet等,以及它们在多线程环境下的性能和使用策略。 4. **...

    java并发编程实践pdf笔记

    Java并发编程实践是Java开发中不可或缺的一个领域,它涉及到如何高效、正确地处理多线程环境中的任务。这本书的读书笔记涵盖了多个关键知识点,旨在帮助读者深入理解Java并发编程的核心概念。 1. **线程和进程的...

    基于JDK源码解析Java领域中的并发锁之设计与实现.pdf

    在Java并发编程中,理解和掌握并发锁的原理与实现至关重要,因为它们是解决多线程环境下的互斥和同步问题的关键。本文将基于JDK源码解析Java领域中的并发锁,探讨AQS基础同步器、LockSupport、Condition接口、Lock...

    实战JAVA高并发程序设计源代码汇总版

    《实战JAVA高并发程序设计源代码汇总版》是针对JAVA并发编程领域的一份宝贵资源,它集合了《实战JAVA高并发程序设计》一书中的核心示例代码,旨在帮助开发者深入理解和实践JAVA并发编程的关键技术。这个压缩包包含了...

    Java 线程同步调用

    在深入探讨Java线程同步调用这一主题之前,我们首先需要理解线程同步的基本概念及其在多线程环境中的重要性。线程同步是多线程编程中的一个关键概念,它确保了多个线程在访问共享资源时不会发生冲突,避免了数据不...

    JAVA并发编程艺术pdf版

    通过深入学习《JAVA并发编程艺术》,开发者能更好地理解并发编程的原理,熟练运用Java提供的并发工具和API,解决实际开发中的多线程问题,提高软件的性能和稳定性。这是一本值得每一位Java开发者研读的书。

    13-Java并发编程学习宝典.zip

    通过学习以上内容,开发者可以深入了解Java并发编程的核心概念,掌握多线程的创建、管理、同步和通信,以及如何利用并发容器和框架优化程序性能。同时,这些知识对于解决实际问题和应对面试中的并发编程题目都极其...

    深入解析Java的线程同步以及线程间通信

    总之,Java的线程同步和线程间通信是多线程编程中的关键概念,通过`synchronized`关键字、同步块和监视器锁,以及`wait()`, `notify()`, `notifyAll()`等方法,可以有效地管理共享资源,避免并发问题,并实现线程间...

    Java并发编程:深入解析抽象队列同步器(AQS)及其在Lock中的应用

    AQS是处理线程同步问题的高效工具,是Java并发编程中的核心。文章首先简要介绍了并发编程领域的先驱Doug Lea。重点在于ReentrantLock的分析,它是基于AQS实现的互斥锁。相比synchronized,它提供了更多特性,如手动...

Global site tag (gtag.js) - Google Analytics