java 多线程并发

来源:互联网 发布:js触发select下拉事件 编辑:程序博客网 时间:2024/06/10 21:17

一、多线程

首先搞清楚容易混淆的两个概念:

 

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。

 

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源

 

二、Java内存模型(Java Memory Model)

1、volatile关键词

Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.

  A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务

public class Worker{     private volatile boolean done;     public void setDone(boolean done){        this.done = done;     }     public void work(){        while(!done){           //执行任务;        }     }  }  


public class Counter {      public volatile static int count = 0;      public static void inc() {          //这里延迟1毫秒,使得结果明显          try {              Thread.sleep(1);          } catch (InterruptedException e) {          }          count++;      }      public static void main(String[] args) {          //同时启动1000个线程,去进行i++计算,看看实际结果          for (int i = 0; i < 1000; i++) {              new Thread(new Runnable() {                  @Override                  public void run() {                      Counter.inc();                  }              }).start();          }          //这里每次运行的值都有可能不同,可能不为1000       System.out.println("运行结果:Counter.count=" + Counter.count);      }  }  


该例子的运行结果不是100

原因如下:

先看图:


图片注解:

上图是JAVA内存模型

目标是定义程序中各个变量的访问规则。(包括实例字段、静态字段和构成数组的元素,不包括局部变量和方法参数)

1.   所有的变量都存储在主内存中(虚拟机内存的一部分)。

2.   每条线程都由自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

3.   线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

规则

1不允许read和load、store和write操作之一单独出现。

2不允许一个线程丢弃最近的assign操作,变量在工作内存中改变了之后必须把该变化同步回主内存中。

3不允许一个线程没有发生过任何assign操作把数据从线程的工作内存同步回主内存中。

4一个新的变量只能在主内存中诞生。

    这4点就造成了该例子的运行结果不是100,还有一些关于lock的规则,本文没有讲到,讲太多不好消化.

 

read and load 从主存复制变量到当前工作内存

use and assign  执行代码,改变共享变量值 
store and write
用工作内存数据刷新主存相关内容

其中use and assign可以多次出现

但是这一些操作并不是原子性,也就是read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的


2 final关键词

方案3可以解决上述问题,不过先请大家看一下2
final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)

当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象

另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取

 

3 java基本类型的原子操作

 

原子操作非阻塞同步

基于冲突检测的乐观并发策略:先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(一般是不断的尝试,直到成功为止)。

 

先举个栗子:

    线程不安全的代码:

package com.etrip.web;    import java.io.IOException;  import java.io.PrintWriter;    import javax.servlet.ServletException;  import javax.servlet.http.HttpServlet;  import javax.servlet.http.HttpServletRequest;  import javax.servlet.http.HttpServletResponse;    /**  * Servlet implementation class CountServlets  */  public class CountServlets extends HttpServlet {      private static final long serialVersionUID = 1L;            //记录访问此Servlet的数量      private  static long count=0;      /**      * @see HttpServlet#HttpServlet()      */      public CountServlets() {          super();          // TODO Auto-generated constructor stub      }        /**      * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)      */      @Override      protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {          PrintWriter pw=response.getWriter();          count++;          pw.print("当前的请求次数为为:"+count);          pw.flush();          pw.close();      }      }  


 

线程安全的代码:

package com.etrip.web;  import java.io.IOException;  import java.io.PrintWriter;  import java.util.concurrent.atomic.AtomicLong;    import javax.servlet.ServletException;  import javax.servlet.http.HttpServlet;  import javax.servlet.http.HttpServletRequest;  import javax.servlet.http.HttpServletResponse;  import java.util.concurrent.atomic.*;  /**  *   *   *  */  public class CountExtServlet extends HttpServlet {      private static final long serialVersionUID = 1L;           //记录访问此Servlet的数量   private  final AtomicLong count=new AtomicLong(0);   /**  * @see HttpServlet#HttpServlet()  */      public CountExtServlet() {          super();          // TODO Auto-generated constructor stub      }        /**      * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)      */      @Override      protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {          PrintWriter pw=response.getWriter();          count.incrementAndGet();          pw.print("当前的请求次数为为:"+count.get());          pw.flush();          pw.close();  }    }  


 

我想此时,大家应该对原子变量也有了解了吧。顺便说一句:

    为了提高性能,AtomicLong等类在实现同步时,没有用synchronized关键字,而是直接使用了最低层(本地c语言实现代码)来完成的,因而你是看不到用synchronized关键字的。

AtomicInteger,AtomicBoolean,AtomicLong,AtomicReference等。这是由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。

 

 

三、Java提供的线程同步方式


1、synchronized关键字

关键字主要解决多线程共享数据同步问题

 synchronized关键字属于互斥同步:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。互斥方式:临界区、互斥量和信号量

方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)

 

实例方法:当前对象实例所关联的监视器对象。

/取钱 

   public  synchronized voidsubMoney(int money){        if(count-money < 0){             System.out.println("余额不足");             return;        }        count -=money;        System.out.println(+System.currentTimeMillis()+"取出:"+money);    } 


通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

synchronized (this) {              if(count-money < 0){                  System.out.println("余额不足");                  return;              }              count -=money;          }          System.out.println(+System.currentTimeMillis()+"取出:"+money);  


2:ThreadLocal

可重入代码:可以在代码执行的任何时刻中断它,转而去执行另一段代码,而在控制权返回后,原来的程序不会出现任何错误。特征:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数传入,不调用非可重入的方法等。如果一个方法,它的返回结果是可以预测的,只要出入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求

 

ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题

 

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。 

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。 

概括起来说,对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLoacl的一般用法,创建一个ThreadLocal的匿名子类并覆盖initalValue(),把ThreadLoacl的使用封装在另一个类中

       publicclass ThreadLocalIdGenerator{           private static finalThreadLocal<IdGenerator> idGenerator = new  ThreadLocal<IdGenerator>(){                 protected IdGeneratorinitalValue(){                    return newIdGenerator();//IdGenerator 是个初始int value =0,然后getNext(){ return value++}                 }              };           public static int getNext(){              return idGenerator.get().getNext();           } }


 

如果大家感兴趣可以继续看一下

1:生产者消费者模式:

使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面的帖子会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。其实当初这个模式,主要就是用来处理并发问题的

2:java.util.concurrent包为多线程提供了高层的API

Lock接口,表示一个锁方法:

ReadWriteLock接口,表示两个锁,读取的共享锁和写入的排他锁。

ReentrantLock类和ReentrantReadWriteLock,分别为上面两个接口的实现类。

3:线程池 :解决多线程创建销毁的消耗问题

如果有时间我一定一一总结给大家,请多提批评意见,共同进步,欢迎拍砖!





 

 

 

0 0
原创粉丝点击