• Java编程思想学习(十六) 并发编程


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

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

    1.线程的状态和切换:

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

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

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

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

     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类线程的方法:

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

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

    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接口线程的方法:

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

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

    3.使用Executors线程池:

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

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

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

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

    4.获取线程的返回值:

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

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

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

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

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

    输出结果:

    Start daemons

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

    输出结果:

    Start daemons

    Finally is running

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

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

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

    8.synchronized线程同步:

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

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

    (1).对象方法同步:

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

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

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

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

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

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

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

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

    (4).类同步代码块:

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

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

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

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

    9.线程锁:

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

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

    输出结果:

    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字段的读写操作实现了线程同步。

  • 相关阅读:
    [唐胡璐]Selenium技巧- Highlight页面元素
    算法进阶面试题07——求子数组的最大异或和(前缀树)、换钱的方法数(递归改dp最全套路解说)、纸牌博弈、机器人行走问题
    算法进阶面试题06——实现LFU缓存算法、计算带括号的公式、介绍和实现跳表结构
    算法进阶面试题05——树形dp解决步骤、返回最大搜索二叉子树的大小、二叉树最远两节点的距离、晚会最大活跃度、手撕缓存结构LRU
    算法进阶面试题04——平衡二叉搜索树、AVL/红黑/SB树、删除和调整平衡的方法、输出大楼轮廓、累加和等于num的最长数组、滴滴Xor
    算法进阶面试题03——构造数组的MaxTree、最大子矩阵的大小、2017京东环形烽火台问题、介绍Morris遍历并实现前序/中序/后序
    算法进阶面试题02——BFPRT算法、找出最大/小的K个数、双向队列、生成窗口最大值数组、最大值减最小值小于或等于num的子数组数量、介绍单调栈结构(找出临近的最大数)
    算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串
    算法初级面试题08——递归和动态规划的精髓、阶乘、汉诺塔、子序列和全排列、母牛问题、逆序栈、最小的路径和、数组累加成指定整数、背包问题
    算法初级面试题07——前缀树应用、介绍和证明贪心策略、拼接字符串得到最低字典序、切金条问题、项目收益最大化问题、随时取中位数、宣讲会安排
  • 原文地址:https://www.cnblogs.com/benchao/p/5273280.html
Copyright © 2020-2023  润新知