Java基础技术核心归纳(三)

来源:互联网 发布:js .get 编辑:程序博客网 时间:2024/06/05 11:04

Java基础技术核心归纳(三)

转载请声明出处:http://blog.csdn.net/andrexpert/article/details/77919005


Android                                          Java                                            数据结构
Android基础技术核心归纳(一)     Java基础技术核心归纳(一)     数据结构基础知识核心归纳(一) 
Android基础技术核心归纳(二)     Java基础技术核心归纳(二)     数据结构基础知识核心归纳(二)  
Android基础技术核心归纳(三)     Java基础技术核心归纳(三)    数据结构基础知识核心归纳(三)  
Android基础技术核心归纳(四)     Java基础技术核心归纳(四)


     不知不觉又是一年的9月,今天跟一个师弟聊天,谈到了他现在面试的一些情况,突然想起自己当年也是这么走过来的,顿时感慨良多。Android/Java经验汇总系列文章,是当初自己毕业时笔试、面试和项目开发中相关的总结,虽然不是很高深的东西,也没有归纳得很全面,但是对Android、算法、Java把握个大概还是没问题,今天特意将这些文章放出来,希望能够对看到这个系列文章的毕业生朋友一点帮助吧。当然,由于受当时知识面的限制,归纳得可能不是很准确,若有疑问就留言哈。


1.Java加密技术
一、MD5
1.MD5简介

    MD5,Message Digest Algorithm 5,即消息摘要算法第五版。MD5属于单向加密算法,是不可逆的加密方式,它是计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护和加密。严格来说,它是一种摘要算法,是确保信息完整性的。不过,在某种意义上来说,也可以算作一种加密算法。MD5作用具体表述如下:
(1)保证消息的完整性
    将要传送的明文通过MD5算法转换成消息摘要(散列函数实现),不同的明文对应不同的信息摘要(MD5值的唯一性)。然后,再将消息摘要与要加密的明文一起传送给接收方,接收方将接收的明文产生新的消息摘要,并与发送方发来的消息摘要(MD5值)进行比较。若比较的结果一致则表示明文未被改动过,如果不一致则说明明文已经被篡改,如文件校验。
(2)加密
      将要传送的明文通过MD5算法转换成消息摘要(MD5值,由散列函数实现),然后将MD5值传送到服务器,并与服务器上保存的MD5值进行比较,从而实现数据加密。很多网站在数据库存储用户的密码的时候都是存储用户密码的MD5值,当用户登录的时候,系统把用户输入的密码计算成MD5值,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确,同时也避免了用户的密码被具有系统管理员权限的用户知道。由于MD5属于不可逆算法且只有明文消息摘要,若想破解是比较难的。  
(3)MD5算法的特点:
    ● 压缩性:任意长度的数据,算出的MD5值长度都是固定的(即定长128位的十六进制数字串);
    ● 容易计算:从原数据计算出MD5值很容易;
    ● 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别;
    ● 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的;
    ● 不可逆:被MD5加密的明文不能被恢复
2.MD5工作原理
     MD5的作用就是把一个任意长度的字节串变换成定长的十六进制数字串(128位)。MD5算法可简要叙述为:MD5 以 512 位分组来处理输入的信息,且每一分组又被划分为 16 个 32 位子分组,经过了一系列的处理后,算法的输出由四个 32 位分组组成,将这四个 32 位分组级联后将生成一个 128 位散列值(MD5值)。
3.Java实现MD5加密

/**  * MD5加密  */  //surce参数是待加密的字符串,encrypt_MD5方法返回加密后的结果public String encrypt_MD5(String source) throws Exception{    //获得MD5摘要算法的 MessageDigest 对象,"MD5"为申明使用MD5算法     MessageDigest md5 = MessageDigest.getInstance("MD5");       //使用指定的字节更新摘要     md5.update(source.getBytes());      //获得密文(字节流,调用digest完成哈希计算),并对其进行Base64编码得到编码串    return Base64.encodingToString(md5.digest(),Base64.DEFAULT);   }
4.MD5安全性分析
    普遍认为MD5是很安全,因为暴力破解的时间是一般人无法接受的。实际上如果把用户的密码MD5处理后再存储到数据库,其实是很不安全的。因为用户的密码是比较短的,而且很多用户的密码都使用生日,手机号码,身份证号码,电话号码等等。或者使用常用的一些吉利的数字,或者某个英文单词。如果我把常用的密码先MD5处理,把数据存储起来,然后再跟你的MD5结果匹配,这时我就有可能得到明文。
参考&MD5实现源码:http://blog.csdn.net/happylee6688/article/details/43953671
二、SHA
1.SHA简介

   SHA,“Secure Hash Algorithm”,即安全散列算法。散列,是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。SHA就是一种单向加密,不可逆,安全的散列算法。它主要适用于数字签名标准(Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。SHA是一个系列,它包括SHA-1、SHA-224、SHA-256、SHA-384、SHA-521等几种算法,其中,SHA-1,SHA-224和 SHA-256适用于长度不超过 2^64 二进制位的消息。SHA-384 和 SHA-512 适用于长度不超过 2^128二进制位的消息。
(1)数字签名实现原理 
    通过散列算法可实现数字签名实现,数字签名的原理是将要传送的明文通过一种函数运算(Hash)转换成报文摘要(不同的明文对应不同的报文摘要),报文摘要加密后与明文一起传送给接受方,接受方将接受的明文产生新的报文摘要与发送方的发来报文摘要解密比较,比较结果一致表示明文未被改动,如果不一致表示明文已被篡改。
(2)单向散列函数的安全性
    其产生散列值的操作过程具有较强的单向性。如果在输入序列中嵌入密码,那么任何人在不知道密码的情况下都不能产生正确的散列值,从而保证了其安全性。
    SHA 将输入流按照每块 512 位(64 个字节)进行分块,并产生20个字节(160位)的被称为信息认证代码或信息摘要的输出。
2.SHA1工作原理
    SHA-1 是一种数据加密算法,主要是接收一段明文,然后以一种不可逆的方式将它转换成一段密文,也可以简单的理解为取一串输入码,并把它们转化为长度较短、位数固定的输出序列即散列值的过程。该算法输入报文的长度不限,产生的输出是一个 160 位的报文摘要。输入是按 512 位的分组进行处理的。SHA-1 是不可逆的、防冲突,并具有良好的雪崩效应。  
SHA-1 有两个特点:
不可以从消息摘要中复原信息(不可逆性)
两个不同的消息,不会产生同样的消息摘要(唯一性)
3.Java实现MD5加密
    MessageDigest 类为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,输出固定长度的哈希值。MessageDigest 对象开始被初始化。该对象通过使用 update 方法处理数据。任何时候都可以调用 reset 方法重置摘要。一旦所有需要更新的数据都已经被更新了,应该调用 digest 方法之一完成哈希计算。  
    对于给定数量的更新数据,digest 方法只能被调用一次。digest 被调用后,MessageDigest 对象被重新设置成其初始状态。
/**  * MD5加密  */  //surce参数是待加密的字符串,encrypt_MD5方法返回加密后的结果public String encrypt_MD5(String source) throws Exception{    //获得MD5摘要算法的 MessageDigest 对象,"MD5"为申明使用MD5算法     MessageDigest md5 = MessageDigest.getInstance("SHA");       //使用指定的字节更新摘要     md5.update(source.getBytes());      //获得密文(字节流,调用digest完成哈希计算),并对其进行Base64编码得到编码串    return Base64.encodingToString(md5.digest(),Base64.DEFAULT);   }
4.MD5与SHA-1比较
    SHA-1和MD5均由MD4导出,它们的强度和其他特性非常相似,主要区别如下:
(1)对强行攻击的安全性。
    最显著和最重要的区别是 SHA-1 摘要比 MD5 摘要长 32 位(MD5-128位,SHA1-160位)。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对 MD5 是 2^128 数量级的操作,而对 SHA-1 则是 2^160 数量级的操作。这样,SHA-1 对强行攻击有更大的强度。
因此,SHA-1的安全性比MD5要高。
(2)对密码分析的安全性。
     由于MD5 的设计,易受密码分析的攻击,SHA-1 显得不易受这样的攻击。
因此,MD5的速度比SHA-1快。
参考&SHA实现源码:http://blog.csdn.net/happylee6688/article/details/43965609
                                   http://blog.csdn.net/forgotaboutgirl/article/details/7258109
                                   http://blog.csdn.net/happylee6688/article/details/43953671
三、HMAC
1.HMAC简介

    HMAC,全称为“Hash Message Authentication Code”,中文名“散列消息鉴别码”,是含有密钥散列函数算法,兼容了MD和SHA算法的特性,并在次基础上加上了密钥。HMAC是有一个公开的协议(约定摘要算法),是一种基于密匙的报文完整性的验证方法,其安全性是建立在Hash算法基础上。它要求通信双方共享密匙、对报文进行Hash运算(如MD5、SHA等),形成固定长度的认证码(MAC,Message Authentication Code),可以为十六进制表示,且长度摘要值的长度与实现算法的摘要值长度相等。通信双方通过认证码的校验来确定报文的合法性,可以用来用作加密、数字签名、报文验证等。
2.HMAC工作原理
    HMAC是一种执行"校验和"的算法,它通过对数据进行"求和"来检查数据是否被更改。在发送数据以前,HMAC算法对输入的数据和双方约定的公钥分别进行"散列操作(如SHA1),生成一个唯一、长度固定的消息摘要(即消息认证码,MAC,Message Authentication Code),并附加在待发送的消息数据中。当数据和摘要达到目的地时,就使用散列函数来生成另一个校验和(消息摘要)。如果两个校验和数据相匹配,那就说明数据在传输或存储过程中未被篡改。
3.HMACSHA1算法的消息认证
(1消息认证过程
● 服务器端向客户端公布摘要算法,如MD5、SHA1等;
● 服务器端与客户端按照约定构造密匙,双方拥有相同的密匙。其中,由服务器端构造密匙后通知客户端,此过程不需要通过程序实现,就是双方约定个字符串,但是这个字符串可不是随便设定的,也是通过相关算法获取的;
● 客户端使用密钥对消息做摘要处理(信息摘要长度MD5为128字节、SHA1为160字节),然后将要传输的消息和生成的摘要一同发送给乙方;
● 服务器收到客户端传递过程的消息数据后,使用已经公布的摘要算法(如MD5、SHA1)和约定好的密钥(字符串) 对收到的消息进行摘要处理。然后,对比自己的摘要消息和客户端发过来的摘要消息,来进行数据安全性校验。    
(2)HMACSHA1加密Java代码实现
   
 /**初始化HmacSHA1的密钥     *    @return byte[] 密钥    **/    public static byte[] initHmacSHAKey() throws Exception{        //a.初始化KeyGenerator,即密钥生成器,并指定使用的摘要算法        KeyGenerator keyGenerator = new KeyGenerator("HmacSHA1");        //b.产生密钥        SecretKey secretKey = keyGenerator.generateKey();        //c.返回密钥字节流        return  secreKey.getEncoded();    }    /**HmacSHA1信息摘要     *    @param data 待做信息摘要处理的数据    *     @param  key 密钥    *     @return byte[] 消息摘要    **/    public static byte[] encodeHmacSHA(byte[] data,byte[] key) throws Exception{        //a.还原密钥,因为密钥是以byte形式为消息传递算法所拥有        SecretKey secretKey = new SecretKeySpec(key,"HmacSHA1");        //b.实例化Mac,用于生成指定算法,如SHA1,信息摘要        Mac mac = Mac.getInstance("secretKey.getAlgorithm()");    //初始化Mac        mac.init(secretKey);    //初始化Mac        //c,执行消息摘要算法,得到信息摘要        return mac.doFinal(data);         }
4.HMACSHA1安全性分析
    基于SHA-1算法的HMAC能够有效地保护传输过程中数据的完整性和有效性,具体表现如下:
(1) 基于一个通信双方共享的密钥。在发送数据前,对密钥(yue) 进行了SHA散列运算。在生成消息摘要的过程中,又对密钥进行了两次异或运算,大大加深了攻击者破译的难度。另外,周期性地动态更新密钥,克服了hash函数和密钥本身潜在的不安全隐患,降低了由此带来的危害,进一步加强了MAC的认证。
(2)基于一个加密的散列函数SHA-1,该函数不仅具有不可逆性(单向性),而且有优越的抗攻击能力和良好的雪崩效应。
(3)HMAC-SHA1算法产生160bit摘要散列,仅有2^64的杂凑,因此它能有效的抵抗穷举攻击,防止数据被任意地纂改和颠覆,最大限度地实现了数据传输的完整性。
(4)通信双方采用了同步序列号,可以有效地进行身份认证,防止数据重传。
(5)在该机制中,对SHA1函数的初始变量、中间结果和最终摘要,都进行了有效的安全的存储和保护,攻击者很难截获。        
   
参考&SHA实现源码:http://blog.csdn.net/happylee6688/article/details/43968549
                                  http://blog.csdn.net/janronehoo/article/details/7590976
四、RSA加密算法
1.算法原理

   RSA加密算法是最常用的非对称加密算法,是第一个比较完善的公开密钥算法,它既能用于加密,也能用于数字签名,RSA是可逆的。RSA的安全基于大数分解的难度。其公钥e和私钥d是一对大素数(100到200位十进制数或更大)的函数,从一个公钥和密文恢复出明文的难度,等价于分解两个大素数之积。即要求私钥d,根据e×d≡1 mod (q-1)(p-1),需要通过qp,求因子p,q.
    为了便于计算。在以下实例中只选取小数值的素数p,q,以及e,假设用户A需要将明文“key”通过RSA加密后传递给用户B,过程如下:
(1)设计公私密钥(e,n)和(d,n),e是公匙,d是密匙。
    令p=3,q=11,得出n=p×q=3×11=33;f(n)=(p-1)(q-1)=2×10=20;取e=3,(3与20互质)则e×d≡1 mod f(n),即3×d≡1 mod 20。 d怎样取值呢?可以用试算的办法来寻找。试算结果见下表: 
   
  通过试算我们找到,当d=7时,e×d≡1 mod f(n)同余等式成立,表示为(e*d) mod f(n) = 1 mod f(n)=1。因此,可令d=7。从而我们可以设计出一对公私密钥,加密密钥(公钥)为:KU =(e,n)=(3,33),解密密钥(私钥)为:KR =(d,n)=(7,33)。
(2)英文数字化。
  将明文信息数字化,并将每块两个数字分组。假定明文英文字母编码表为按字母顺序排列数值,即:
      
  则得到分组后的key的明文信息为:11,05,25。
(3)明文加密
  用户加密密钥(3,33) 将数字化明文分组信息加密成密文。由得: 
       C1= (M1)^e(mod n) = 11^3(mod 33)=11
       C2= (M2)^e(mod n) = 5^3(mod 33)=31
       C3= (M3)^e(mod n) = 25^3(mod 33)=16
  因此,得到相应的密文信息为:11,31,16。
(4)密文解密
  用户B收到密文,若将其解密,只需要计算,其中C1=11,C2=21,C3=16,d=7,n=33
    
  用户B得到明文信息为:11,05,25。根据上面的编码表将其转换为英文,我们又得到了恢复后的原文“key”。 当然,实际运用要比这复杂得多,由于RSA算法的公钥私钥的长度(模长度)要到1024位甚至2048位才能保证安全,因此,p、q、e的选取、公钥私钥的生成,加密解密模指数运算都有一定的计算程序,需要仰仗计算机高速完成。
2.密匙产生过程
(1)选择一对不同的、足够大的素数p,q。
(2)计算n=pq,其中n是公开的
(3)计算f(n)=(p-1)(q-1),同时对p, q严加保密,不让任何人知道。
(4)随机选择加密密钥 e ,要求 e 和 (p-1)*(q-1)互质且1<e<f(n)。
(5)利用 Euclid 算法计算解密密钥 d , 使其满足 e*d = 1(mod(p-1)*(q-1)) (其中 n,d 也要互质)
(6)公钥KU=(e,n),私钥KR=(d,n)
注释1:素数,也称质数
    素数是这样的整数,它除了能表示为它自己和1的乘积以外,不能表示为任何其它两个整数的乘积。例如,15=3*5,所以15不是素数
注释2:互质数
    公约数只有1的两个数(即自然数),叫做互质数。如两个质数一定是互质数(2与7)、1不是质数也不是合数,它和任何一个自然数在一起都是互质数等。
注释3:摸指数运算
    模指数运算就是先做指数运算,取其结果再做模运算。如5^3 mod 7 = 125 mod 7 =6
参考:http://blog.csdn.net/yanzi1225627/article/details/26508035
            http://bank.hexun.com/2009-06-24/118958531.html

一、多线程基本理论
1.多线程的概念

     一个程序可能包含多个并发运行的任务。线程(thread)是指一个任务从头至尾的执行流,线程提供了运行一个任务的机制,多线程可以使程序反应更快、交互性更强、执行效率更高(提供任务执行并发实现)。在Java中,每个任务都是Runnable接口的一个实例,也称为可运行对象(running object),线程就是一个便于任务执行的对象,可以通过实现Runnable接口来定义任务类,通过使用Thread构造方法包住一个任务来创建线程---任务在线程中执行。一个线程是一个程序内部的一条执行线索(一部分代码),如果要一程序中实现多段代码同时交替运行,就需产生多个线程,并指定每个线程上所要运行的程序代码,即为多线程。
2.线程的生命周期(状态)
    线程的生命周期包括:新建、就绪、运行、阻塞和消亡五种状态。

说明:
(1)新创建一个线程,即进入新建状态;
(2)调用线程的start()方法启动线程后,线程进入就绪状态(Ready);
(3)操作系统调度该处于就绪状态的线程并为其分配CPU时间,线程进入运行状态;
(4)如果给定的CPU时间片用完或调用线程的yield()方法,处于运行状态线程进入就绪状态;
(5)当线程自己调用join()方法或sleep()方法或wait()方法、等待I/O操作完成等,线程进入阻塞状态;
(6)当线程执行完其run()方法后或main()方法结束,这个线程就被结束了。
3.Runnable接口与Thread类
(1)Runnable接口
    在Java中每个任务都是Runnable接口的一个实例,即任务就是一个可运行的Runnable对象。为了创建任务,必须首先为任务定义一个类,任务类必须实现Runnale接口(只包含一个run方法)。
public class TaskClass implements Runnable{   ......    public TaskClass(..){   }    public void run(){     /*       * 任务模块       */    }} 
注意:任务中的run()方法指定如何完成这个任务,Java虚拟机会自动调用该方法,无需特意调用它。另外,直接调用run()方法只是在同一线程中执行该方法,而没有新线程被启动。
(2)Thead类
    由于任务必须在线程中运行,Thead类就是一个用来创建线程的类并提供了很多控制线程的方法。常用方法如下:
    ◆Thread():创建一个空线程
    ◆Thread(Runnable task):为指定任务创建一个线程
    ◆void start():启动线程,使方法run()被JVM调用
    ◆boolean isAlive():检测线程当前是否正在运行
    ◆void setPriority(int p):设置线程的优先级p(范围从1到10),Thread类有Int型常量MIN_PRIORITY、NORM_PRIORITY和MAX_PRIORITY分别代表1、5、10.
    ◆void join():等待线程结束
    ◆void sleep(long mills):使线程睡眠指定时间,期间线程释放所占用的处理机
    ◆void yield():使线程暂时并允许执行其他线程
    ◆void interrupt():中断线程
>创建、启动线程:
    TaskRunnale  task = TaskRunnable(....);    //创建一个任务对象    Thread thread = new Thread(task );           //创建一个线程    thread.start();                                            //启动线程
注意:Thread类还包含方法stop()、suspend()和resume(),由于可能存在潜在的安全问题,为替代这些方法,建议通过给Thread变量赋值Null来表明该线程被停止。
4.线程池(thread pools)
    对于单一任务来说,使用new Thread(new Runnable(..)).start()方法还是比较方便的。但是对于大量任务来说,要为每个任务创建一个新线程就会消耗一定的资源并且造成性能降低。 为此,Java提供了线程池来管理多任务,通过Executor接口来执行线程池中的任务和ExecutorService接口来管理和控制任务。
(1)Executor接口
    ◆void execute(Runnable task):执行指定的任务
(2)ExecutorService接口
    ◆void shutdown():关闭执行器,但允许完成执行器中的任务,一旦关闭,它就不能接受新任务;
    ◆List<Runnable task> shutdownNow():即使线程池中还有未完成的线程,还是会立即关闭执行器,返回未完成的任务的清单
    ◆boolean isShutdown():如果执行器已经被关闭则返回true
    ◆boolean isTerminated():如果线程池中所有的任务都被终止,则返回true
注:ExecutorService继承于Executor接口,故ExecutorService对象可调用execute(Runnable task)执行任务。
(3)Executors接口
    ◆static ExecutorService newFixedThreadPool(int numberOfThead):创建一个线程池,该线程池可并发执行的线程数固定不变。在线程的当前任务结束后,它可以被重用以执行另一个任务。 
    ◆static ExecutorService newFixedThreadPool():创建一个线程池,它可按需创建新线程,但当前面创建的线程可用时,可重用它们;
(4)创建一个线程池实例
class ExecutorDemo{     //创建线程池,线程个数为3个    ExectorService executor =    Executors.newFixedThreadPool(3);       //创建多个任务添加到同一个线程池中,执行器比并发执行这三个任务    executor.executor(new Task1(...));    executor.executor(new Task2(...));    executor.executor(nw Task3(...));    //关闭执行器,不接收新任务,现有的任务继续执行直至结束    executor.shutdown();}   
注意:
◆若将创建线程池语句改为:ExectorService executor=Executors.newFixedThreadPool(1)。执行器将顺序执行这个可运行的任务,因为在线程池中只有一个线程。
◆若将创建线程池语句改为:ExectorService executor=Executors.newFixedThreadPool()。为每个等待的任务创建一个新线程,为此所有的任务都并发地执行。
总之,如果需要为一个任务创建一。一个线程,就使用Thread类。如果需要为多个任务创建线程,最好使用线程池。
5.Android引入线程目的   
   如果在主线程中执行耗时任务(操作),所有的绘制以及输入事件将被阻塞,直到该操作完成,这将导致Android应用程序出现"ANR"(Application Not Response)异常。为了确保永远不要阻塞主线程,需要将执行文件、数据库或者访问网络等耗时操作放在另一个单独的线程中执行。
二、创建线程的两种方法
    Java中创建多线程主要有两种方法:继承Thread类和实现Runnable接口。
1.用Thread类创建线程
(1)原理
    Java的线程是通过java.lang.Thread类来控制的,一个Thread类的对象代表一个线程,而且只能代表一个线程,通过Thread类和它定义的对象,我们可以获得当前线程对象、设置线程的优先级或获取某一线程的名称或者控制线程暂停一段时间等功能。
(2)开发思路
a.实现一个类,使该类继承于Thread类;
b.实现该类的run()方法,用于实现我们希望线程要完成的任务;
c.实例化该类一个对象,调用其start()方法启动该线程。
(3)源码举例
//主线程  public class DemoThread  {   public static void main(String arg[])   {    TestThread thread = new TestThread(); //创建一个Thread子类对象    thread.start();   //启动该子线程    while(true)    {     System.out.println("main thread is running.");    }   }  }    //子线程的类  class TestThread extends Thread  {   public void run() {    while(true)    {     System.out.println(Thread.currentThread().getName()+" is running.");    }   }  }  
分析:通过观察程序运行情况可知,程序有两个线程(多线程)无规律的交替运行。
效果:

升华笔记1
1.要实现多线程,我们必须编写一个继承了Thread类的子类,子类必须覆盖Thread类中的run()方法,并且在子类的run方法中编写我们希望在新线程上实现的功能代码;
2.启动一个新的线程不是直接调用Thread子类对象的run()方法,二是调用Thread子类对象的start()方法,Thread类对象的start方法将产生一个新的线程,并在该线程上运行该Thread类对象中的run方法。
3.由于线程的代码段在run方法中,当run()方法执行完成后,相应的线程也就结束了,所以我们可以通过控制run方法中的循环条件来控制线程的终止。
2.使用Runnable接口创建多线程
(1)原理
    当使用Thread(Runnale target)方法创建线程对象时,需为该方法传递一个实现了Runnable接口的类对象,这样创建的线程将调用哪个实现了Runnable接口的类对象中的run()的方法作为其运行代码,而不在调用Thread类中的run方法。
(2)开发思路
a.实现一个类,该类继承于Runnable接口并覆盖实现Runnable接口的run()方法;
b.实例化该类一个对象,并将该对象作为参数实例化一个Thread类对象;
c.调用Thread类的start()方法启动该子线程;
(3)源码举例
//主线程  public class RunnableTest {   public static void main(String args[])   {    TestRunnable runnable = new TestRunnable();   //创建一个类对象,该类继承于Runnable接口    Thread thread=new Thread(runnable);   //创建一个带Runnable对象参数的线程    thread.start();   //启动该线程    while(true)    {     System.out.println("main thread is running.");    }   }  }  //子线程  class TestRunnable implements Runnable  {   @Override   public void run() {    while(true)    {     System.out.println(Thread.currentThread().getName()+" is running.");    }   }     }  
效果:

3.两种实现多线程方式的对比分析
    通过上述两个例子,我们知道继承Thread类和实现Runnable接口都能实现多线程,并且也未看出两者有何区别。但是,当我们希望开发的多线程去处理同一资源(一个资源只能对应一个对象)时,继承Thread类方式就有点力不从心了。
    举个例子:通过程序来模拟铁路售票系统,实现通过四个售票点发售某日某次列车的100张车票,一个售票点用一个线程来表示。
(1)继承Thread类方式的多线程
//主线程  public class SaleTickets {   public static void main(String[] arg)   {    new Resource().start();//启动线程1    new Resource().start();//启动线程2    new Resource().start();//启动线程3    new Resource().start();//启动线程4      }  }  //唯一资源  class Resource extends Thread  {   private int tickets=100;   //100张票   @Override   public void run() {     //线程每运行一次run方法,票数减1    while(tickets>0)    {      System.out.println(Thread.currentThread().getName()+" is saling tickets "+tickets--);    }   }     }  
分析:观察结果,我们知道在主线程中创建的四个线程各自卖各自的100张票,而不是去卖共同的100张票,也就是说这几个线程去处理的不是同一资源,实例化4个thread对象,就在堆内存中为其分配不同地址的内存!实际上,在上面的程序中,我们创建了四个 Resource对象,就等于创建了四个资源,每个 Resource对象中都有100张票,每个线程在独立地处理各自的资源。
效果:

(2)继承Runnable类方法的多线程
//主线程  public class SaleTickets {   public static void main(String[] arg)   {    Resource r = new Resource();  //实例化一个Runnbale子类对象,代表一个资源    /*依次创建4个子线程*/    new Thread(r).start();    new Thread(r).start();    new Thread(r).start();    new Thread(r).start();      }  }  //唯一资源  class Resource implements Runnable  {   private int tickets=100;   //100张票   @Override   public void run() {     //线程每运行一次run方法,票数减1    while(tickets>0)    {      System.out.println(Thread.currentThread().getName()+" is saling tickets "+tickets--);    }   }     }  
分析:上面程序实现了模拟铁路售票系统的要求。从上面程序中可以看出,我们只创建了一个资源对象 Resource(该对象中包含要发售的那100张票)。然后,我们在创建四个线程,每个线程去调用同一个 Resource对象中的run()方法,因此这四个线程访问的是同一个对象中的变量(tickets)的实例,从而实现了多线程访问同一资源的功能。
效果:

升华笔记2:实现Runnable接口相对于继承Thread类优势?

1.实现Runnable接口适合多个相同程序代码的线程去处理同一资源的情况,把程序的代码、数据有效分离,较好地体现了面向对象的设计思想;
2.实现Runnable接口有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个先的执行代码来自同一个类的实例时,即称他们共享相同的代码。多个线程可以操作相同的数据,于他们(线程)的代码无关。当共享访问相同的对象时,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。
三、守护(后台)线程与联合线程
1.后台进程

(1)概述:
    在上面的售票系统的例子中,我们在main方法中创建并启动新的线程后,main方法便结束了,主线程也就随着结束了。但是,我们知道Java程序在运行的过程中只要还有一个前台线程在运行,整个Java程序(进程)没有随之结束,子线程的宿主进程便成为了一个空进程。当然,如果我们将该启动的新线程设置为后台进程,如果一个进程中只有该后台线程运行,这个线程便随着main方法的结束而结束。(子线程中main方法中被执行)
设置某个进程为后台进程方法:
  Thread thread=new Thread(runnable); 
  thread.setDaemon(true);                    //设置子进程为守护进程
  thread.start();
(2)源码举例
//主线程  public class RunnableTest {   public static void main(String[] args)   {    int i=10;    TestRunnable runnable = new TestRunnable();   //创建一个类对象,该类继承于Runnable接口    Thread thread=new Thread(runnable);   //创建一个带Runnable对象参数的线程    thread.setDaemon(true);    thread.start();   //启动该线程    while((--i>0))    {     System.out.println("main thread is running.");    }   }  }  //子线程  class TestRunnable implements Runnable  {   @Override   public void run() {    while(true)    {     System.out.println(Thread.currentThread().getName()+" is running.");    }   }     }  
效果演示:

2.联合线程
    所谓联合线程,就是当一个线程调用join()[如pp.join(),其中pp为线程对象方法后,把pp所对应的线程合并到调用pp.join()语句的线程中。
(1)Thread类的方法:
void join():合并线程
void join(long millis):线程合并millis毫秒后分离到合并前状态
void join(long millis,int nanos):线程合并millis毫秒nanos纳秒后分离到合并前状态
(2)源码举例
public class joinThead {      public static void main(String[] args)   {    RunnableTask mRunnable = new RunnableTask ();  //创建一个Runnable子类对象    Thread thread=new Thread(mRunnable );   //创建一个线程    thread.start();  //启动线程    int i=100;    while(i>0)    {     if(i==50)     {      try {       thread.join();      } catch (InterruptedException e) {       e.printStackTrace();      }     }     System.out.println("main thread:"+i--);    }   }  }  class RunnableTask implements Runnable  {   private int i=100;   public void run() {    while(i>0)    {     i--;     System.out.println(Thread.currentThread().getName()+": "+i );    }   }  }  
效果分析:

    经观察可知,当主线程的i递减=50时,子线程与主线程合并,此时只有子线程中的i在计算,直到子线程run方法代码运行完全两个线程再分离。最后,主线程继续执行后面的代码。


线程同步与死锁
    在上一篇文章中,有一个模拟售卖火车票系统,在卖车票的程序代码中,极有可能碰到一种意外,就是同一张票号被打印两次多次,也可能出现打印出0甚至负数的票号。具体表现为:假设tickets的值为1的时候,线程1刚执行完if(tickets>0)这行代码,正准备执行下面的代码,就在这时,操作系统将CPU切换到了线程2上执行,此时tickets的值仍为1,线程2执行完上面两行代码,tickets的值变为0后,CPU又切回到了线程1上执行,线程1不会再执行if(tickets>0)这行代码,因为先前已经比较过了,并且比较的结果为真,线程1继续执行后面的代码,最终导致tickets的值为0,而这个结果是我们不允许的。
一、线程同步(多线程同步操作同一资源)
1.线程同步

    为了解决上述多线程操作同一资源出现不同步的问题,我们可以这样做,即当一个线程运行到if(tickets>0)后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的有关代码块。这段代码就好比一座独木桥,任意时刻,只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。
2.synchronized ['sɪŋkrənaɪz]语句--互斥锁
格式:synchronized(object){代码段}    //pbject可以是任意的一个对象
     synchronized语句内的代码段,就形成了同步代码块。也就是说,在同一时刻只能有一个线程可以进入同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内运行。object为任意类型的对象,该对象都有一个标志位,该标志位具有0、1两种状态(0-忙,1-空闲),其开始状态为1,当执行synchronized(object)语句后,object对象的标志位变为0状态,知道执行完整个synchronized语句中的代码块后又回到了1状态。一个线程执行到synchronized(object)语句处时,先检查object对象的标志位(即同步对象的锁标旗),如果为0状态,表明已经有另外的线程的执行状态正在有关的同步代码块中,这个线程将暂时阻塞,让出CPU资源,知道另外的线程执行完有关的同步代码块,将object对象的标志位恢复到1状态这个阻塞就被取消。
    一个用于synchronized语句中的对象称为一个监视器,当一个线程获得了synchronized(object)语句中的代码块的执行权,即意味着它锁定了监视器,在一段时间内,只能有一个线程可以锁定监视器。当同步块代码执行完毕或者遇到break语句或抛出异常时,线程也会释放该锁旗标。
synchronized语句同步代码实现:
public class DemoThread {    public static void main(String[] args)    {     ThreadTest runnable = new ThreadTest();   //声明一个Runnable子类对象      new Thread(runnable).start();    //创建四个线程,它们使用的是同一资源      new Thread(runnable).start();     new Thread(runnable).start();     new Thread(runnable).start();    }  }  //实现一个Runnable子类  public class ThreadTest implements Runnable {   int tickets=100;   String str = new String();    //定义一个锁旗标   public void run()   {     while(true)     {      synchronized(str)      {       if(tickets>0)       {        try        {         Thread.sleep(1000);  //尽管获得锁旗标的线程睡眠,但其他线程仍无法进入同步代码块       //等睡眠时间到,该线程会继续执行下面的代码,直到抛出异常、遇break、执行完全才会放弃占用的锁旗标      }        catch(Exception e)        {         e.printStackTrace();        }        System.out.println(Thread.currentThread().getName()+" is saling tickets"+tickets--);       }      }     }   }  }  
分析:如果我们将synchronized同步对象的锁标旗在run()方法中定义时,多线程不能实现同步。那是因为当一个线程启动后将会调用run方法,对每一次调用,程序都产生一个不同的str局部对象,这四个线程使用的同步监视器完全是四个不同的对象,所以彼此之间不能同步。
3.不同代码块相互同步
    上面我们提到的同步代码块,是指不仅同一个代码块在多个线程间实现同步。当然,若干个不同的代码块也可以实现相互之间的同步,只要各synchronized(object)语句中的object完全是同一个对象就可以了。
4.同步函数
    除了可以对代码块进行同步外,也可以对函数实现同步,即只要在需要同步的函数定义前加上synchronized关键字即可。
(1)DemoThread.java
public class DemoThread {    public static void main(String[] args)    {     ThreadTest runnable = new ThreadTest();   //声明一个Runnable子类对象      new Thread(runnable).start();    //创建四个线程,它们使用的是同一资源      new Thread(runnable).start();     new Thread(runnable).start();     new Thread(runnable).start();    }  }  
(2)ThreadTest.java
//实现一个Runnable子public class ThreadTest implements Runnable  {   int tickets=100;   public void run()   {     while(true)     {      sale();     }   }   public synchronized void  sale()   {     if(tickets>0)     {      try      {       Thread.sleep(1000);      }      catch(Exception e)      {       e.printStackTrace();      }      System.out.println(Thread.currentThread().getName()+" is saling tickets"+tickets--);     }   }  }  
分析1:在同一类中,使用synchronized关键字定义的若干方法,可以在多个线程之间同步,当有一个线程进入了synchronized修饰的方法(获得监视器),其他线程就不能进入同一个对象的所有使用了synchronized修饰的方法,直到第一个线程执行完它所进入的synchronized修饰的方法未知(离开监视器)。
分析2:我们通过观察程序结果发现,程序的运行速度要比原来没有使用同步处理时慢,那是因为系统要不停的对同步监视器进行检测,需要更多时间的开销。所以说同步是以牺牲程序的性能为代价的,如果我们能够确定程序没有安全性问题,就没必要使用同步控制。
分析3:在实现代码块与函数之间的同步时,由于同步函数使用的监视器是this对象(类中的非静态方法始终都能访问到的一个对象就是这个对象本身即this),所以同步代码块中应使用this对象来作为同步监视器。

二、线程死锁
    死锁是一种少见的、而且难以调试的错误,在两个线程对两个同步对象具有循环依赖时具就会出现死锁。例如一个线程进入对象X的监视器,而另一个对象进入了对象Y的监视器,这时进入X对象监视器的线程如果还试图进入Y对象的监视器就会被阻隔,接着进入Y对象监视器的线程如果试图进入X对象的监视器也会被阻隔,这样两个线程都处于挂起状态。
(1)A.java
    
public class A {   synchronized void foo(B b)   {        String name = Thread.currentThread().getName();   //获取当前线程的名称        System.out.println(name+"enter A.foo");       try       {             Thread.sleep(100);       }       catch(Exception e)      {           e.printStackTrace();      }          System.out.println(name+" is trying to call b.last");          b.last();      }   synchronized  void last()   {    System.out.println("inside A.last");   }  }  
(2)B.java
public class B {   synchronized void bar(A a)   {    String name = Thread.currentThread().getName();   //获取当前线程的名称    System.out.println(name+"enter B.bar");    try    {     Thread.sleep(100);    }    catch(Exception e)    {     e.printStackTrace();    }    System.out.println(name+" is trying to call b.last");    a.last();   }   synchronized  void last()   {    System.out.println("inside B.last");   }  }  
(3)BlockRunnable.java
public class BlockRunnable implements Runnable  {   A a=new A();    //创建一个A类对象   B b=new B();    //创建一个B类对象   BlockRunnable()  //构造方法   {    Thread.currentThread().setName("MainThread");  //设置主线程名字    new Thread(this).start();  //创建并启动一个子线程    a.foo(b);  //主线程中调用a类的foo方法,foo方法试图调用B类的bar方法    System.out.println("back in the Main thread");   }   public void run() {    Thread.currentThread().setName("ChildThread");    b.bar(a);    System.out.println("back in the ChildThread");   }   //主   public static void main(String[] args)   {    new BlockRunnable();   }  }  


线程间通信(基于同步)与生命控制
一、线程通信方法
    Java是通过Object类的wait、notify、notifyAll这几个方法来实现线程间的通信。由于所有的类都是从Object继承的,因此在任何类中都可以直接使用这些方法。
wait:告诉当前线程放弃监视器并进入睡眠状态,直到其他线程进入同一监视器并调用notify为止;
notify:唤醒同一对象监视器中调用wait的第一个线程。用于类似饭馆有一个空位后通知所有等候就餐的顾客中的第一位可以入座的情况;
notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有最高优先级的线程首先被唤醒并执行,用于类似某个不定期的培训班终于招生满额后,通知所有学员都来上课的情况。
注意:这里所谓的同一对象监视器,是指线程间通信的synchronized(object)中的object对象为同一个对象。


二、生产者-消费者模型
1.简介

生产者Producer.java:当数据库数据被消费者读取后,向一个数据库写入数据;
消费者Customer.java:当数据库有数据且生产者写入完成时,读取里面的数据;
数据库Data.java:一个类,提供读取和写入两种方法,并且为线程同步和同一对象监视器
主线程ThreadCommunication.java:创建两个线程,线程共享同一资源Data.
2.功能源码
实现:Producer线程存放依次数据,Consumer线程就取一次,反之,Producer也必须等到Consumer取完后才能存放新的数据。
(1)Data.java
public class Data  
{  

}   



(2)Producer.java

public class Producer implements Runnable  {   Data data=null;   public Producer(Data data) //构造方法   {    this.data=data;   }   public void run() {    int i=0;    while(true)    {     if(i==0)     {      data.put("吴倩","女");     }     else     {      data.put("林俊杰","男");     }     i=(i+1)%2;    }   }  }  
(3)Customer.java
//消费者类class Customer implements Runnable  {   Data data=null;   public Customer(Data data) {    this.data=data;   }   public void run()   {    while(true)    {     data.get();    }   }  }  
(4)ThreadCommunication.java
//主类public class ThreadCommunication  {   public static void main(String[] args)   {    Data data = new Data();    new Thread(new Producer(data)).start();   //生产者线程    new Thread(new Customer(data)).start();//消费者线程   }  }  
效果演示:

总结:wait、notify、notifyAll这三个方法只能在synchronized方法中调用,即无论线程调用一个对象的wait还是notify方法,该线程必须先得到该对象的锁旗标。这样,notify只能唤醒同一对象监视器中调用wait的线程,使用多个对象监视器,我们就可以分组有多个wait、notify的情况,同组里的wait只能被同组的notify唤醒。
三、线程生命的控制
1.线程的运行   

     一个线程的产生是从我们调用了start方法开始进入Runnable状态,即可以被调度运行状态,并没有真正开始运行,调度器可以将CPU分配给它,真正运行其中的程序代码。线程在运行过程中,有以下几个可能的去向:
(1)没有遇到任何阻隔,运行完成直到结束,也就是run()方法执行完毕;
(2)调度器将CPU分配给其他线程,这个线程又变为Runnable状态;
(3)请求锁旗标,却得不到,这时候它要等待对象的锁旗标,得到锁旗标后会进入Runnable状态开始运行;
(4)遇到wait方法,他会被放入等待池中继续等待,直到有notify()或interrupt()方法执行,它才会别唤醒或打断开始等待对象锁旗标,等到锁旗标后进入Runnable状态继续执行。
2.控制线程生命
    控制线程生命周期的方法有很多种,如:suspend方法,resume方法和stop方法。但是,一般不推荐使用这三种方法,其中,不推荐使用suspend和resume是因为:
(1)会导致死锁的发生;
(2)它允许一个线程通过直接控制另外一个线程的代码来直接控制哪个线程。
另外,虽然stop能够避免死锁的发生,但是带来了另外的不足,如果一个线程正在操作共享数据段,操作过程没有完成就stop了的话,将会导致数据的不完整性。
下面我们介绍一种方法---设置标识,来控制线程的生命周期:
threadRunnable.java
public class threadRunnable implements Runnable  {   boolean stopFlag=true;  //定义一个标识   //停止线程方法   public void stopMe()   {    stopFlag=false;   }   //run方法   public void run()   {     while(stopFlag)     {      System.out.println(Thread.currentThread().getName()+" is running");     }   }  }  
ThreadLife.java
public class ThreadLife  {   public static void main(String[] args)   {    threadRunnable runnable = new threadRunnable();    new Thread(runnable).start();    for(int i=0;i<100;i++)    {       if(i==50)  //当i累加到50时,结束子线程       {         runnable.stopMe();       }       System.out.println("MainThread is running");    }   }  }  
效果演示:当i累加到50时,子线程结束。


原创粉丝点击