《Java编程思想》学习笔记18——并发编程(一)

http://blog.csdn.net/chjttony/article/details/7039602

线程是进程中一个任务控制流序列,由于进程的创建和销毁需要销毁大量的资源,而多个线程之间可以共享进程数据,因此多线程是并发编程的基础。

多核心CPU可以真正实现多个任务并行执行,单核心CPU程序其实不是真正的并行运行,而是通过时间片切换来执行,由于时间片切换频繁,使用者感觉程序是在并行运行。单核心CPU中通过时间片切换执行多线程任务时,虽然需要保存线程上下文,但是由于不会被阻塞的线程所阻塞,因此相比单任务还是大大提高了程序运行效率。

1.线程的状态和切换:

线程的7种状态及其切换图如下:

2.多线程简单线程例子:

Java中实现多线程常用两种方法是:实现Runnable接口和继承Thread类。

(1).继承Thread类的多线程例子如下:

[java] view plaincopy

  1. class PrimeThread extends Thread {  
  2. long minPrime;  
  3.          PrimeThread(long minPrime) {  
  4. this.minPrime = minPrime;  
  5.          }  
  6. //重写Thread类的run方法
  7. public void run() {  
  8.               . . .  
  9.          }  
  10.      }  

启动继承Thread类线程的方法:

[java] view plaincopy

  1. PrimeThread p = new PrimeThread(143);  
  2. p.start();  

(2).实现Runnable接口的多线程例子如下:

[java] view plaincopy

  1. class PrimeRun implements Runnable {  
  2. long minPrime;  
  3. PrimeRun(long minPrime) {  
  4. this.minPrime = minPrime;  
  5. }  
  6. public void run() {  
  7. . . .  
  8. }  
  9. }  

启动实现Runnable接口线程的方法:

[java] view plaincopy

  1. PrimeThread p = new Thread(new PrimeThread(143));  
  2. p.start();  

由于java的单继承特性和面向接口编程的原则,建议使用实现Runnable接口的方式实现java的多线程。

3.使用Executors线程池:

在JDK5中,在java.util.concurrent包中引入了Executors线程池,使得创建多线程更加方便高效,例子如下:

[java] view plaincopy

  1. import java.util.concurrent.*;  
  2. public class CachedThreadPool{  
  3. public static void main(String[] args){  
  4. //创建一个缓冲线程池服务
  5.         ExecutorService exec = Executors.newCachedThreadPool();  
  6. for(int i=0; i<5; i++){  
  7. //线程池服务启动线程
  8.     exec.execute(  
  9. new Runnable(){  
  10. //使用匿名内部类实现的java线程
  11. public void run(){  
  12.         System.out.println(“Thread ” + i + “ is running”);  
  13. }  
  14. }  
  15. );  
  16. //线程池服务停止
  17.         exec.shoutdown();  
  18. }  
  19. }  
  20. }  

Executors.newCachedThreadPool()方法创建缓冲线程池,即在程序运行时创建尽可能多需要的线程,之后停止创建新的线程,转而通过循环利用已经创建的线程。Executors.newFixedThreadPool(intsize)方法创建固定数目的线程池,即程序会创建指定数量的线程。缓冲线程池效率和性能高,推荐优先考虑使用。

Executors.newSingleThreadPool()创建单线程池,即固定数目为1的线程池,一般用于长时间存活的单任务,例如网络socket连接等,如果有多一个任务需要执行,则会放进队列中顺序执行。

4.获取线程的返回值:

实现Runnable接口的线程没有返回值,如果想获取线程的返回值,需要实现Callable接口,Callable是JDK5中引入的现实线程的接口,其call()方法代替Runnable接口的run方法,可以获取线程的返回值,例子如下:

[java] view plaincopy

  1. import java.util.concurrent.*;  
  2. import java.util.*;  
  3. class TaskWithResult implements Callable<String>{  
  4. private int id;  
  5. public TaskWithResult(int id){  
  6. this.id = id;  
  7. }  
  8. public String call(){  
  9. return “result of TaskWithResult ” + id;  
  10. }  
  11. public static void main(String[] args){  
  12.     ExecutorService exec = Executors.newCachedThreadPool();  
  13.     List<Future<String>> results = new ArrayList<Future<String>>();  
  14. for(int i=0; i<5; i++){  
  15. //将线程返回值添加到List中
  16.         results.add(exec.submit(new TaskWithResult(i)));  
  17. }  
  18. //遍历获取线程返回值
  19. for(Future<String> fs : results){  
  20. try{  
  21.     System.out.println(fs.get());  
  22. }catch(Exception e){  
  23.     System.out.println(e);  
  24. }finally{  
  25.     exec.shutdown();  
  26. }  
  27. }  
  28. }  
  29. }  

输出结果(可能的结果,由于多线程执行顺序不确定,结果不固定):

result of TaskWithResult 0

result of TaskWithResult 1

result of TaskWithResult 3

result of TaskWithResult 4

result of TaskWithResult 5

注解:使用线程池服务的submit()方法执行线程池时,会产生Future<T>对象,其参数类型是线程Callable的call()方法返回值的类型,使用Future对象的get()方法可以获取线程返回值。

5.线程休眠:

在jdk5之前,使用Thread.sleep(1000)方法可以使线程休眠1秒钟,在jdk之后,使用下面的方法使线程休眠:

TimeUnit.SECONDS.sleep(1);

线程休眠的方法是TimeUnit枚举类型中的方法。

注意:不论是Thread.sleep还是TimeUnit的sleep线程休眠方法,都要捕获InterruptedExecutors。

6.线程优先级:

线程的优先级是指线程被线程调度器调度执行的优先级顺序,优先级越高表示获取CPU允许时间的概率越大,但是并不是绝对的,因为线程调度器调度线程是不可控制的,只是一个可能性的问题。可以通过Thread线程对象的getPriority()方法获取线程的优先级,可以通过线程对象的setPriority()方法设置线程的优先级。

Java的线程优先级总共有10级,最低优先级为1,最高为10,Windows的线程优先级总共有7级并且不固定,而Sun的Soloaris操作系统有23级,因此java的线程优先级无法很好地和操作系统线程优先级映射,所有一般只使用MAX_PRIORITY(10),NORM_PRIORITY(5)和MIN_PRIORITY(1)这三个线程优先级。

7.守护线程:

守护线程(DaemonThread)是某些提供通用服务的在后台运行的程序,是优先级最低的线程。当所有的非守护线程执行结束后,程序会结束所有的守护线程而终止运行。如果当前还有非守护线程的线程在运行,则程序不会终止,而是等待其执行完成。守护进程的例子如下:

[java] view plaincopy

  1. import java.util.concurrent.*;  
  2. public class SimpleDaemons implements Runnable{  
  3. public void run{  
  4. try{  
  5. System.out.println(“Start daemons”);  
  6. TimeUtil.SECONDS.sleep(1);  
  7. }catch(Exception e){  
  8. System.out.println(“sleep() interrupted”);  
  9. }finally{  
  10.     System.out.println(“Finally is running”);  
  11. }  
  12. }  
  13. public static void main(String[] args) throws Exception{  
  14. Thread daemon = new Thread(new SimpleDaemons());  
  15.     daemon.setDaemon(true);  
  16.     daemon.start();  
  17. }  
  18. }  

输出结果:

Start daemons

Finally没有执行,如果注释掉daemon.setDaemon(true)设置守护进程这一句代码。

输出结果:

Start daemons

Finally is running

之所以产生这样的结果原因是,main()是这个程序中唯一的非守护线程,当没有非守护线程在运行时,JVM强制推出终止守护线程的运行。

通过Thread对象的setDaemon方法可以设置线程是否为守护线程,通过isDaemon方法可以判断线程对象是否为守护线程。

由守护线程创建的线程对象不论有没有通过setDaemon方法显式设置,都是守护线程。

8.synchronized线程同步:

编程中的共享资源问题会引起多线程的竞争,为了确保同一时刻只有一个线程独占共享资源,需要使用线程同步机制,即使用前对共享资源加锁,使用完毕之后释放锁。

Java中通过synchronized关键字实现多线程的同步,线程同步可以分为以下几种:

(1).对象方法同步:

[java] view plaincopy

  1. public synchronized void methodA(){       
  2.         System.out.println(this);       
  3.     }    

每个对象有一个线程同步锁与之关联,同一个对象的不同线程在同一时刻只能有一个线程调用methodA方法。

(2).类所有对象方法同步:

[java] view plaincopy

  1. public synchronized static void methodB(){       
  2.         System.out.println(this);       
  3.     }    

静态方法的线程同步锁对类的所有对象都起作用,即所有对象的线程在同一时刻只能有一个类的一个线程调用该方法。

(3).对象同步代码块:

[java] view plaincopy

  1. public void methodC(){    
  2. synchronized(this){    
  3.             System.out.println(this);    
  4.         }    
  5.     }    

使用当前对象作为线程同步锁,同一个对象的不同线程在同一时刻只能有一个线程调用methodC方法中的代码块。

(4).类同步代码块:

[java] view plaincopy

  1. public void methodD(){    
  2. synchronized(className.class){    
  3.             System.out.println(this);    
  4.         }    
  5.     }    

使用类字节码对象作为线程同步锁,类所有对象的所有线程在同一时刻只能有一个类的一个线程调用methodD的同步代码块。

注意:线程的同步是针对对象的,不论是同步方法还是同步代码块,都锁定的是对象,而非方法或代码块本身。每个对象只能有一个线程同步锁与之关联。

如果一个对象有多个线程同步方法,只要一个线程访问了其中的一个线程同步方法,其它线程就不能同时访问这个对象中任何一个线程同步方法。

9.线程锁:

JDK5之后,在java.util.concurrent.locks包中引入了线程锁机制,编程中可以显式锁定确保线程间同步,例子如下:

[java] view plaincopy

  1. import java.util.concurrent.*;  
  2. import java.util.concurrent.locks.*;  
  3. public class Locking{  
  4. //创建锁
  5. private ReentrantLock lock = new ReentrantLock();  
  6. public void untimed(){  
  7. boolean captured = lock.tryLock();  
  8. try{  
  9.     System.out.println(“tryLock(): ” + captured);  
  10. }finally{  
  11. if(captured){  
  12.     lock.unlock();  
  13. }  
  14. }  
  15. }  
  16. public void timed(){  
  17. boolean captured = false;  
  18. try{  
  19. //对象锁定两秒钟
  20.     captured = lock.tryLock(2, TimeUnit.SECONDS);  
  21. }catch(InterruptedException e){  
  22.     Throw new RuntimeException(e);  
  23. }try{  
  24.     System.out.println(“tryLock(2, TimeUnit.SECONDS): ” + captured);  
  25. }finally{  
  26. if(captured){  
  27.     lock.unlock();  
  28. }  
  29. }     
  30. }  
  31. public static void main(String[] args){  
  32. //主线程
  33. final Locking al = new Locking();  
  34.     al.untimed();  
  35.     al.timed();  
  36. //创建新线程
  37. new Thread(){  
  38.     {//动态代码块,对象创建时执行
  39. setDaemon(true);//当前线程设置为守护线程
  40.             }  
  41. public void run(){  
  42. //获取al对象的线程锁并锁定al对象
  43.                 al.lock.lock();  
  44.         System.out.println(“acquired”);  
  45. }  
  46. }.start();  
  47. //主线程让出CPU
  48. Thread.yield();  
  49. al.untimed();  
  50.     al.timed();  
  51. }  
  52. }  

输出结果:

tryLock(): true

tryLock(2, TimeUnit.SECONDS): true

acquired

tryLock(): false

tryLock(2, TimeUnit.SECONDS): false

由于创建的守护线程锁定对象之后没有释放锁,所有主线程再也无法获取对象锁。

ReentrantLock可以通过lock()锁定对象,也可通过tryLock()方法来锁定对象,对于显式使用线程锁的方法体或代码块必须放在try-catch-finally块中,必须在finally中释放对象锁。

ReentrantLock和Synchronized功能是类似的,区别在于:

(1). Synchronized代码简单,只需要一行代码即可,不用try-catch-finally捕获异常,同时不用显式释放对象锁。

(2).ReentrantLock可以控制锁的锁定和释放状态,也可以指定锁定时间等。

10.volatile传播性:

volatile关键字确保变量的跨程序可见性,使用volatile声明的字段,只要发生了写改动,所有读取该字段的值都会跟着改变,即使使用了本地缓存仍然会被改变。

volatile字段会立即写入主内存中,所有的读取值都是从主内存中读取。

volatile关键字告诉编译器移除线程中的读写缓存,直接从内存中读写。volatile适用的情况:

字段被多个任务同时访问,至少有一个任务是写操作。

volatile字段的读写操作实现了线程同步。

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

FlaskJinja2 开发中遇到的的服务端注入问题研究 II

0x00. 前言 本篇文章是 《Flask Jinja2 开发中遇到的的服务端注入问题研究》<点击阅读原文查看链接>续篇,我们继续研究 Flask Jinja...

29360
来自专栏noteless

-1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),

java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法...

11740
来自专栏拭心的安卓进阶之路

并发编程1:全面认识 Thread

线程简介 现在操作系统在运行一个程序时,会自动为其创建一个进程,不论是 PC 还是 Android。 一个进程内可以有多个线程,这些线程作为操作系统调度的最小单...

23250
来自专栏TechBox

【iOS】运行时消息传递与转发机制前言(一)对象的消息传递机制 objc_msgSend()(二)消息转发流程参考文章

14240
来自专栏C语言及其他语言

C语言中的预处理

1、 宏定义 预处理命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中...

38960
来自专栏CSDN技术头条

Java 并发编程之美-线程相关的基础知识

借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了;相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在...

20130
来自专栏haifeiWu与他朋友们的专栏

Redis协议规范(译文)

Redis客户端使用名为RESP(Redis序列化协议)的协议与Redis服务器进行通信。 虽然该协议是专为Redis设计的,但它可以用于其他CS软件项目的通讯...

15630
来自专栏pythonlove

Bash脚本编程(原创)

Bash,Unix shell的一種,在1987年由布萊恩·福克斯為了GNU計劃而编写。1989年釋出第一個正式版本,原先是計劃用在GNU作業系統上,但能运行于...

12230
来自专栏java 成神之路

JVM 类加载机制深入浅出

280110
来自专栏cloudskyme

什么是线程安全

什么是线程安全?       如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且...

33380

扫码关注云+社区

领取腾讯云代金券