线程基础知识系列(三)线程的同步

本文是系列的第三篇,前面2篇,主要是针对单个线程如何管理,启动等,没有过多涉及多个线程是如何协同工作的。

线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等

线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务。

本文的主要内容

  1. 何谓线程安全?
  2. 何谓共享可变变量?
  3. 认识synchronized关键字
  4. 认识Lock
  5. synchronized vs Lock

1.何谓线程安全

多线程是把双刃剑,带来高效的同时,也带来了安全隐患。什么是线程安全?众说一次,很多版本的说辞。引用《Java并发编程实战》书中的定义,如下:当多线程访问时,永远都能表现正确的行为。延伸解读下“何谓正确性”。正确性就是不管是多线程访问,还是单线程访问,影响的结果是一致的。可以将单线程的正确性形容为“所见即所知”。借助下面的例子解释下。

SysnExampleV1.java

package com.threadexample.sysn;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class SysnExampleV1 {
   static class Task implements  Runnable{
        private Integer count=0;
        private int cycleSize;
        public Task(int cycleSize) {
            this.cycleSize=cycleSize;
        }
        @Override
        public void run() {
            for(int i=0;i<cycleSize;i++){
                this.count++;
            }
        }
       private void doSomething(){
           final Random random =new Random();
           try {
               TimeUnit.MILLISECONDS.sleep(random.nextInt(10));
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }

       public int getCount(){
           return this.count;
       }
    }
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task(1000);
        Thread t1=new Thread(task);
        Thread t2=new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("计数(线程数*循环数)="+task.getCount());
    }
}

Task 类维护一个实例变量count,作为计数器。每循环一次计数加1.一共启用2个线程,每个线程循环1000次。为了保证线程完整执行调用线程的join(),最后的预期效果:2*1000=2000.

测试结果如下(而且结果经常变化)

计数(线程数*循环数)=1958

根据“所见即所知”,2个线程,每个循环1000次,当然是2000了。可结果不是2000.说明Task类不是线程安全的。

简单剖析下原因: 问题出在this.count++,这个操作是符合操作。

使用自带的javap -v SysnExampleV1$Task.class 命令查看字节码文件 ,可以很容易找到原因。

     ...
       42: getfield      #3                  // Field count:Ljava/lang/Integer;
       45: astore        4
       47: aload_3
       48: aload_3
       49: getfield      #3                  // Field count:Ljava/lang/Integer;
       52: invokevirtual #12                 // Method java/lang/Integer.intValue:()I
       55: iconst_1
       56: iadd
       57: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       60: dup_x1
       61: putfield      #3                  // Field count:Ljava/lang/Integer;
       ...

简单的一个自增操作,被分解为

  1. 获取当前c的值;
  2. 对获取到的值加1;
  3. 把递增后的值写回到c;

既然是复合操作,一个线程更新了数据,还没有保存到共享缓存,另外一个线程这时候读取数据,就会存在拿到过期数据的情况。简单演示下,假设count c初始化为0,

线程A:获取c;
线程B:获取c;
线程A:对获取的值加1,结果为1;
线程B:对获取的值加1,结果为1;
线程A:结果写回到c,c现在是1;
线程B:结果写回到c,c现在是1;

按正常理解,B应该写回2才正确。

接下来如何解决这个问题呢。其实很简单。就是用synchronized处理。在介绍synchronized之前,先简单说一下共享变量。

2.何谓共享可变变量

要编写线程安全的代码,其核心在于对状态访问操作的管理上,特别是对共享的和可变的状态的访问。“共享”意味着可以由多个线程同时访问,而”可变“意味着变量的值在其生命周期内可以发生变化。根据不同分类,简单介绍几种变量

局部变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

public void someMethod(){
  
  long threadSafeInt = 0;

  threadSafeInt++;
}

局部的对象引用

对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。所以存在变量逸出现象。关于逸出的相关知识,可以参考《JAVA并发编程实战》3.2节“发布与逸出”。

public void someMethod(){
  
  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创
建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个
someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如
果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。

对象成员

对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();
    
    public add(String text){
        this.builder.append(text);
    }
}

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题.这时候如果多线程访问对象成语变量,线程就不是线程安全的,所以需要使用java提供的加锁机制进行保护了。目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea。本文并不比较synchronized与Lock孰优孰劣。

3.认识synchronized关键字

synchronized是java的一个关键字。java提供了2个同步机制,同步方法和同步块。同步块要关联一个保护的对象,同步方法关联的是this对象。受同步保护的代码块,一次只允许一个线程进入,至于JVM底层又是如何实现synchronized的。

同步机制的建立是基于其内部一个叫内部锁或者监视锁的实体。(在Java API规范中通常被称为监视器。)内部锁在同步机制中起到两方面的作用:对一个对象的排他性访问;建立一种happens-before关系,而这种关系正是可见性问题的关键所在。

每个对象都有一个与之关联的内部锁。通常当一个线程需要排他性的访问一个对象的域时,首先需要请求该对象的内部锁,当访问结束时释放内部锁。在线程
获得内部锁到释放内部锁的这段时间里,我们说线程拥有这个内部锁。那么当一个线程拥有一个内部锁时,其他线程将无法获得该内部锁。其他线程如果去尝试获得
该内部锁,则会被阻塞。

当线程释放一个内部锁时,该操作和对该锁的后续请求间将建立happens-before关系。

更多的原理解释,可以参考深入JVM锁机制之一:synchronized和相关Java memory model知识。

  1. 使用synchronized块保护方法

    代码块1.1

public void run() {
    synchronized (this){
        for(int i=0;i<cycleSize;i++){
            this.count++;
        }
    }
}

或者

代码块1.2

public void run() {
        for(int i=0;i<cycleSize;i++){
            synchronized (this){
            this.count++;
            }
        }
    }

两个方法的区别是synchronized块的保护范围区别,结果是一样的,前者保护的范围大一些,但上下文切换少一些;后者与之相反。具体哪个形式更好,具体要看保护的代码块逻辑了。

2.将方法用synchronized声明

代码块2.1

public synchronized void run() {
        for(int i=0;i<cycleSize;i++){
            this.count++;
        }
    }

这种方式,保护效果与代码块1.1达到的效果是一样的。

3.在类级别synchronized 进行保护

代码块3.1

public  void run() {
    synchronized (SysnExampleV2.class) {
        for (int i = 0; i < cycleSize; i++) {
            this.count++;
        }
    }
}

这种方式是4种种最差的。因为它的保护范围最高,并发性最差。此种情景,不适合此种方式。类级别的加锁,一般使用在单例模式(双重校验锁)。一句话,要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性,简单性和性能。

synchronized不能修饰构造函数。

4.认识Lock

与synchronized不同,要手动创建锁,释放锁,获取锁。如下

Lock lock = new ReentrantLock(); 
lock.lock(); //critical section 
lock.unlock();

SysnExampleV3.java, 展示了Lock的用法。

package com.threadexample.sysn;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SysnExampleV3 {
   static class Task implements  Runnable{
       private final Lock lock = new ReentrantLock();
        private Integer count=0;
        private int cycleSize;
        public   Task(int cycleSize) {
            this.cycleSize=cycleSize;
        }
        @Override
        public void run() {
            for(int i=0;i<cycleSize;i++){
                try {
                    if(lock.tryLock(10, TimeUnit.SECONDS)){
                        this.count++;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }

            }
        }
        //一般这样实现
        /*
                public void run() {
                    for(int i=0;i<cycleSize;i++){
                        lock.lock();  // block until condition holds
                        try {
                            this.count++;
                        } finally {
                            lock.unlock();
                        }
                    }
                }*/

       public int getCount(){
           return this.count;
       }
    }
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task(1000);
        Task task2 = new Task(1000);
        Thread t1=new Thread(task);
        Thread t2=new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("计数(线程数*循环数)="+task.getCount());
    }
}

锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。java提供了以下的锁。

这几种锁的区别与原理,本文不做深入探讨。

5.synchronized vs Lock

  1. synchronized同步块 不提供超时功能,Lock提供了超时功能,使用Lock.tryLock(long timeout, TimeUnit timeUnit)
  2. synchronized同步块,使用简单快捷,这一点也造成了它的滥用。可以配合使用wait(),notify()。lock属于JUC的一部分。
  3. synchronized造成的线程阻塞,可以被dump,而lock造成的线程阻塞不能dump。
  4. synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
  5. lock有公平锁,非公平锁之分。synchronized只有非公平
  6. synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁;Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。
  7. lock额外提供了Conditon

资源

http://ifeve.com/synchronization/

http://ifeve.com/locks/

http://blog.csdn.net/natian306/article/details/18504111

时间: 2024-10-12 21:14:32

线程基础知识系列(三)线程的同步的相关文章

线程基础知识系列(四)线程的同步2 线程通信和Condition变量

本文是系列的第四篇. 线程基础知识系列(三)线程的同步  :同步控制,锁及synchronized 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务. 第三篇文章,重点阐述了如何使用锁和同步块对线程间共享可变变量保护,保证只有一个线程可以进入临界区.其实,没有过多的涉及另一个重要的同步概念:线程协作.第三篇中涉及的线程间并没有有效的协调.

线程基础知识系列(五)认识volatile

线程基础知识系列(四)线程的同步2  :线程的notify-wait通信机制,以及Condition条件变量 线程基础知识系列(三)线程的同步  :同步控制,锁及synchronized 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务. 本篇文章主要讨论的关键字是volatile. volatile使用场景 volatile介绍 vol

线程基础知识系列(二)线程的管理

本篇是线程基础知识系列的第二篇,主要简单江夏线程管理相关知识点. 线程基础知识系列(一)线程的创建和启动:说明了线程的2种创建和启动,join(),daemon线程,Callable 任务. 本文的主要内容 线程的状态 线程的优先级 sleep vs wait 线程的流程控制 Interrupt yield让出你的CPU 1.线程的状态 以<线程基础知识系列(一)线程的创建和启动>这张图,是程序的运行时线程信息截图.有main线程,user Threads,daemon Threads.现在咱

线程基础知识

什么是线程: 在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是"一个进程内部的控制序列" 一切进程至少都有一个执行线程 进程与线程 进程是资源竞争的基本单位 线程是程序执行的最小单位 线程共享进程数据,但也拥有自己的一部分数据 线程ID 一组寄存器 栈 errno 信号状态 优先级 fork和创建新线程的区别 当一个进程执行一个fork调用的时候,会创建出进程的一个新拷贝,新进程将拥有它自己的变量和它自己的PID.这个新进程的运行时间是独立的,它在执行时几乎

Java__线程---基础知识全面实战---坦克大战系列为例

今天想将自己去年自己编写的坦克大战的代码与大家分享一下,主要面向学习过java但对java运用并不是很熟悉的同学,该编程代码基本上涉及了java基础知识的各个方面,大家可以通过练习该程序对自己的java进行一下实战. 每个程序版本代码中,都附有相关注释,看完注释大家就可以对本程序设计有个很明显的思路.真的很有趣,想对java重新温习的同学完全不必再对厚厚的java基础书籍进行阅读了,在跟着本次代码练习并分析后,大家一定会对java各方面基础知识 尤其是线程的知识有更深一步的了解!!! 本次坦克大

Java基础知识的三十个经典问答

Java基础知识的三十个经典问答 1.面向对象的特点 抽象: 抽象是或略一个主题中与当前目标的无关的因素,一边充分考虑有关的内容.抽象并不能解决目标中所有的问题,只能选择其中的一部分,忽略其他的部分.抽象包含两个方面:一是过程抽象:一是数据抽象. 继承 继承是一种联接类的层次模型,允许和鼓励类的重用,提供了一种明确的共性的方法.对象的一个新类可以从现有的类中派生,这叫做类的继承.心累继承了原始类 的特性,新类称为原始类的派生类或者是子类,原始类称为新类的基类或者父类.子类可以从父类那里继承父类的

web基础知识(三)关于ajax,Jquery传值最基础东西

今天补充一下两个小功能,一个是关于radio单选框的情况,如何在前面选了后,传到后台,编辑修改的时候再次传回来,并且在当时选的那个上:再一个就是关于添加小标签的时候添加接着弹出在下面,并点击出现删除. 一:radio 1 <div class="newlylist"> 2 <div class="newlyhead">图示商品:</div> 3 <div class="newlycontent">&

spring基础知识(三)——aop

spring基础知识(三)--aop面向切面编程 1.概念术语 aop面向切面编程(Aspect ariented Programming) 在开始之前,需要理解Spring aop 的一些基本的概念术语(总结的个人理解,并非Spring官方定义): 切面(aspect):用来切插业务方法的类. 连接点(joinpoint):是切面类和业务类的连接点,其实就是封装了业务方法的一些基本属性,作为通知的参数来解析. 通知(advice):在切面类中,声明对业务方法做额外处理的方法. 切入点(poin

C#编译基础知识(三)

本文章我们将来重点介绍强命名程序集,强命名程序集的出现其实是为解决版本控制问题,比如说,在新版程序集发布后,我们希望在系统中对旧程序集的引用继续保留,而有些地方又可以引用新的程序集,再比如说不同的公司提供了不同功能的程序集,这些类库存放在一个公共目录,有时候可能会出现名称相同的情况.使用强命名程序集可以解决这些问题,一个强命名的程序集是靠公钥标示.程序集版本号.区域属性.程序集名称这四个属性来唯一标识的,这样一来,新发布的库文件版本与前面发布的不同,不同的版本引用可以在元数据里面标识,相互不会受