java 多线程并发

一、多线程

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

 

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

 

线程:表示程序的执行流程,是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:线程池 :解决多线程创建销毁的消耗问题

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





 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值