多线程(高级篇)

线程池

Java5中对Java线程的类库做了大量的扩展,其中线程池就是Java5的新特征之一,除了线程池之外,还有很多多线程相关的内容,为多线程的编程带来了极大便利。为了编写高效稳定可靠的多线程程序,线程部分的新增内容显得尤为重要。

有关Java5线程新特征的内容全部在java.util.concurrent下面,里面包含数目众多的接口和类。

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

Java5的线程池分好多种:固定尺寸的线程池、单任务线程池、可变尺寸连接池、延迟线程池等。

在使用线程池之前,必须知道如何去创建一个线程池,在Java5中,需要了解的是java.util.concurrent.Executors类的API,这个类提供大量创建连接池的静态方法,很有用。

固定大小的线程池

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
 
public class Test{
        public static void main(String[] args){
                //创建一个可重用固定线程数的线程池
                ExecutorService pool =Executors.newFixedThreadPool(2);
                //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
                Thread t1 = new Thread(new MyRunnable("ThreadA"));
                Thread t2 = new Thread(new MyRunnable("ThreadB"));
                Thread t3 = new Thread(new MyRunnable("ThreadC"));
                Thread t4 = new Thread(new MyRunnable("ThreadD"));
                Thread t5 = new Thread(new MyRunnable("ThreadE"));
                //将线程放入池中进行执行
                pool.execute(t1);
                pool.execute(t2);
                pool.execute(t3);
                pool.execute(t4);
                pool.execute(t5);
                //启动一次,顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。
                pool.shutdown();
        }
}
 
class MyRunnable implements Runnable{
     
      private String name;
     
      public MyRunnable(String name){
             this.name = name;
      }
        @Override
        public void run() {
                System.out.println("正在执行的线程:"+name);
        }
}

打印结果:

正在执行的线程:ThreadA

正在执行的线程:ThreadB

正在执行的线程:ThreadC

正在执行的线程:ThreadE

正在执行的线程:ThreadD

可见,线程池并不保证按照线程加入池中的顺序来执行

 

Executors.newFixedThreadPool()创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

单任务线程池

如果在上例中使用

ExecutorService pool = Executors.newSingleThreadExecutor();

来创建线程池,即是为单任务线程池。

执行效果类似,不同的是,单任务线程池可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

可变尺寸的线程池

如用

ExecutorService pool = Executors.newCachedThreadPool();

来创建线程池,即为可变尺寸的线程池。

该方法创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。

可调度线程池

可调度线程池可安排线程在给定延迟后运行命令或者定期地执行。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
 
public class Test{
        public static void main(String[] args){
            //创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
            //注意,返回的事ScheduledExecutorService接口,而非ExecutorService,ScheduledExecutorService是ExecutorService的子接口
            ScheduledExecutorService pool =Executors.newScheduledThreadPool(2);
            //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
            Thread t1 = new Thread(new MyRunnable("ThreadA"));
            Thread t2 = new Thread(new MyRunnable("ThreadB"));
            Thread t3 = new Thread(new MyRunnable("ThreadC"));
            Thread t4 = new Thread(new MyRunnable("ThreadD"));
           
 
            //将线程放入池中进行执行
            pool.execute(t1);
            //使用延迟执行的方法:使线程t2延迟2秒再执行
            pool.schedule(t2, 2000,TimeUnit.MILLISECONDS);
            //使用周期执行的方法:使线程t3延迟两秒执行,然后每隔5秒执行一次
            pool.scheduleAtFixedRate(t3, 2000,5000,TimeUnit.MILLISECONDS);
            //使用周期延迟的方法:使线程t4延迟两秒执行,然后,在每一次执行终止和下一次执行开始之间都存在给定的5秒延迟
            pool.scheduleWithFixedDelay(t4,2000, 5000, TimeUnit.MILLISECONDS);
            //如关闭线程池,则周期性的方法只会执行一次
            //pool.shutdown();
        }
}
 
class MyRunnable implements Runnable{
     
      private String name;
     
      public MyRunnable(String name){
             this.name = name;
      }
        @Override
        public void run() {
                System.out.println("正在执行的线程:"+name);
        }
}
打印结果如下:

正在执行的线程:ThreadA

正在执行的线程:ThreadB

正在执行的线程:ThreadC

正在执行的线程:ThreadD

正在执行的线程:ThreadC

正在执行的线程:ThreadD

正在执行的线程:ThreadD

正在执行的线程:ThreadC

正在执行的线程:ThreadC

正在执行的线程:ThreadD

正在执行的线程:ThreadC

正在执行的线程:ThreadD

……

可调度单任务线程池

ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

创建的即为单任务可调度线程池。使用方法与上例类似。

自定义线程池

线程池类java.util.concurrent.ThreadPoolExecutor的常用构造方法为:

ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)

参数意义如下:

corePoolSize: 线程池维护线程的最少数量

maximumPoolSize:线程池维护线程的最大数量

keepAliveTime: 线程池维护线程所允许的空闲时间

unit: 线程池维护线程所允许的空闲时间的单位

workQueue: 线程池所使用的缓冲队列

handler: 线程池对拒绝任务的处理策略

 

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

1、如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

2、如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

3、如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

4、如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

5、当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

 

排队有三种通用策略:

1、直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集合时出现锁定。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

2、无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙的情况下将新任务加入队列。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

3、有界队列。当使用有限的maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

 

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:

NANOSECONDS:毫微妙,千分之一微妙

MICROSECONDS:微妙,千分之一毫秒

MILLISECONDS:毫秒,千分之一秒

SECONDS:秒

HOURS :小时

DAYS :天

 

workQueue常用的是:java.util.concurrent.ArrayBlockingQueue

 

handler有四个选择:

1、ThreadPoolExecutor.AbortPolicy

          用于被拒绝任务的处理程序,它将抛出RejectedExecutionException.

2、ThreadPoolExecutor.CallerRunsPolicy

          用于被拒绝任务的处理程序,它直接在execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。

3、ThreadPoolExecutor.DiscardOldestPolicy

          用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。

4、ThreadPoolExecutor.DiscardPolicy

          用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

 

此类提供 protected 可重写的 beforeExecute(java.lang.Thread,java.lang.Runnable) 和afterExecute(java.lang.Runnable, java.lang.Throwable) 方法,这两种方法分别在执行每个任务之前和之后调用。它们可用于操纵执行环境;例如,重新初始化ThreadLocal、搜集统计信息或添加日志条目。此外,还可以重写方法 terminated() 来执行 Executor 完全终止后需要完成的所有特殊处理。

 

ThreadPoolExecutor可以使我们根据实际需要创建合适的线程池,使程序员编写出更有弹性的代码。

实例:

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class Test
{
 
    private static int queueDeep = 4;
 
    public void createThreadPool()
    {
        /*
         * 创建线程池,最小线程数为2,最大线程数为4,线程池维护线程的空闲时间为3秒,
         * 使用队列深度为4的有界队列,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,
         * 然后重试执行程序(如果再次失败,则重复此过程),里面已经根据队列深度对任务加载进行了控制。
         */
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable> (queueDeep),
                new ThreadPoolExecutor.DiscardOldestPolicy());
 
        // 向线程池中添加 10 个任务
        for (int i = 0; i < 10; i++)
        {
            try
            {
                Thread.sleep(1);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            while (getQueueSize(tpe.getQueue())>= queueDeep)
            {
                System.out.println("队列已满,等3秒再添加任务");
                try
                {
                    Thread.sleep(3000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            TaskThreadPool ttp = new TaskThreadPool(i);
            System.out.println("puti:" + i);
            tpe.execute(ttp);
        }
 
        tpe.shutdown();
    }
 
    private synchronized int getQueueSize(Queue queue)
    {
        return queue.size();
    }
 
    public static void main(String[] args)
    {
        Test test = new Test ();
        test.createThreadPool();
    }
 
    class TaskThreadPool implements Runnable
    {
        private int index;
 
        public TaskThreadPool(int index)
        {
            this.index = index;
        }
 
        public void run()
        {
           System.out.println(Thread.currentThread() + " index:" +index);
            try
            {
                Thread.sleep(3000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
}

打印结果如下:

put i:0

put i:1

Thread[pool-1-thread-1,5,main]index:0

Thread[pool-1-thread-2,5,main]index:1

put i:2

put i:3

put i:4

put i:5

队列已满,等3秒再添加任务

Thread[pool-1-thread-1,5,main]index:2

Thread[pool-1-thread-2,5,main]index:3

put i:6

put i:7

队列已满,等3秒再添加任务

Thread[pool-1-thread-1,5,main]index:4

Thread[pool-1-thread-2,5,main]index:5

put i:8

put i:9

Thread[pool-1-thread-1,5,main]index:6

Thread[pool-1-thread-2,5,main]index:7

Thread[pool-1-thread-1,5,main]index:8

Thread[pool-1-thread-2,5,main]index:9

ThreadFactory

以上创建线程池的构造方法都可以接受一个ThreadFactory接口,它可以根据需要创建新线程的对象,就无需再手工编写对 new Thread 的调用了,从而允许应用程序使用特殊的线程子类、属性等,也可能初始化属性、名称、守护程序状态、ThreadGroup 等等。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test {
     
      public static void main(String[] args) {
             ExecutorService pool =Executors.newCachedThreadPool();
 
//为线程实例命名
             Thread thread1 = new Thread(new MyRunnable(),"ThreadA");
             Thread thread2 = new Thread(new MyRunnable(),"ThreadB");
             pool.execute(thread1);
             pool.execute(thread2);
             pool.shutdown();
            
      }
 
}
class MyRunnable implements Runnable{
            
             @Override
             public void run() {  //打印当前线程的名字
                    System.out.println("线程名字:"+Thread.currentThread().getName());
                                        
             }
      }



 打印结果:

线程名字:pool-1-thread-1

线程名字:pool-1-thread-2

 

可见,我们创建线程实例时给线程的命名并没有生效,这是为什么呢?实际上,如果我们没有传入一个ThreadFactory参数给线程池的构造方法,则会使用一个默认的ThreadFactory,它会给新建线程并设置线程的优先级,新线程具有可通过 pool-N-thread-M的名称,其中 N 是此工厂的序列号,M 是此工厂所创建线程的序列号。就是说,我们之前设置的线程名字被覆盖掉了。

下面我们传入自己写的ThreadFactory,在newThread()方法中可以设置新建线程的参数,也可以进行其他操作。

import java.util.concurrent.ThreadFactory;
 
public class MyThreadFactory implements ThreadFactory {
 
      @Override
      public Thread newThread(Runnable r) {
             Thread thread = new Thread(r);
             //再次设置线程的新名字
             thread.setName("newThreadName");
             return thread;
      } 
  
}
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test {
     
      public static void main(String[] args) {
             //传入自定义的ThreadFactory
             ExecutorService pool =Executors.newCachedThreadPool(new MyThreadFactory());
             Thread thread1 = new Thread(new MyRunnable(),"ThreadA");
             Thread thread2 = new Thread(new MyRunnable(),"ThreadB");
             pool.execute(thread1);
             pool.execute(thread2);
             pool.shutdown();
            
      }
 
}

打印结果:

线程名字:newThreadName

线程名字:newThreadName

现在,线程使用的是我们在MyThreadFactory中设定的新名称。

BlockingQueue

在上例中提到了ArrayBlockingQueue即是BlockingQueue接口的实现类。java.util.concurrent.BlockingQueue继承了java.util.Queue接口。

阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。

有了这样的功能,就为多线程的排队等候的模型实现开辟了便捷通道。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
 
public class Test {
        public static void main(String[]args)throws InterruptedException {
                BlockingQueue<Integer> bqueue = new ArrayBlockingQueue<Integer>(10);
                for (int i = 0; i < 20; i++){
                        //将指定元素添加到此队列中,如果没有可用空间,将一直等待(如果有必要)。
                        bqueue.put(i);
                       System.out.println("向阻塞队列中添加了元素:" + i);
                }
               
                System.out.println("程序到此运行结束,即将退出----");
        }
}

打印结果:

向阻塞队列中添加了元素:0

向阻塞队列中添加了元素:1

向阻塞队列中添加了元素:2

向阻塞队列中添加了元素:3

向阻塞队列中添加了元素:4

向阻塞队列中添加了元素:5

向阻塞队列中添加了元素:6

向阻塞队列中添加了元素:7

向阻塞队列中添加了元素:8

向阻塞队列中添加了元素:9

BlockingQueue的容量为10,存入第十个元素之后已满,所以会进入阻塞状态,等待有可用的空间。

 

除了阻塞队列,还有阻塞栈java.util.concurrent.BlockingDeque接口。不同点在于栈是“后入先出”的结构,每次操作的是栈顶,而队列是“先进先出”的结构,每次操作的是队列头。

Callable与Future

Callable与 Future 两功能是Java在后续版本中为了适应多并法才加入的,Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其他线程执行的任务。

 

Callable的接口定义如下:

 public interface Callable<V> {
 
      V  call()   throws Exception;
 
}

Callable和Runnable的区别如下:

 

1、Callable定义的方法是call,而Runnable定义的方法是run。

2、Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。

3、Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。 

 

Future表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。Future的cancel方法可以取消任务的执行,它有一布尔参数,参数为 true 表示立即中断任务的执行,参数为 false 表示允许正在运行的任务运行完成。Future的 get 方法等待计算完成,获取计算结果

方法列表如下:

boolean cancel(boolean mayInterruptIfRunning)

          试图取消对此任务的执行。

 V get()

          如有必要,等待计算完成,然后获取其结果。

 V get(long timeout, TimeUnit unit)

          如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。

 boolean isCancelled()

          如果在任务正常完成前将其取消,则返回true。

 boolean isDone()

          如果任务已完成,则返回 true。

 

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Test{
 
    public static class  MyCallable implements Callable{
 
          private int flag = 0;
 
          public MyCallable(int flag){
 
                  this.flag = flag;
 
          }
 
          public String call() throws Exception{
 
              if (this.flag == 0){ 
 
                      return "flag =0";
 
            }
 
            if (this.flag == 1){  
 
                try {
 
                    while (true) {
 
                           System.out.println("looping.");
 
                            Thread.sleep(2000);
 
                    }
 
                } catch (InterruptedException e) {
 
                             System.out.println("Interrupted");
 
                }
 
                return "false";
 
            } else {  
 
                       throw new Exception("Bad flag value!");
 
            }
 
        }
 
    }
 
    public static void main(String[] args) {
 
       // 定义3个Callable类型的任务
 
        MyCallable task1 = new MyCallable(0);
 
        MyCallable task2 = new MyCallable(1);
 
        MyCallable task3 = new MyCallable(2);
 
       
 
       // 创建一个执行任务的服务
 
        ExecutorService es =Executors.newFixedThreadPool(3);
 
        try {
 
           // 提交并执行任务,任务启动时返回了一个Future对象,
 
            // 如果想得到任务执行的结果或者是异常可对这个Future对象进行操作
 
            Future future1 = es.submit(task1);
 
           // 获得第一个任务的结果,如果调用get方法,当前线程会等待任务执行完毕后才往下执行
 
            System.out.println("task1:" + future1.get());
 
           
 
            Future future2 = es.submit(task2);
 
           // 等待5秒后,再停止第二个任务。因为第二个任务进行的是无限循环
 
            Thread.sleep(5000);
 
            System.out.println("task2cancel: " + future2.cancel(true));
 
           
 
           // 获取第三个任务的输出,因为执行第三个任务会引起异常
 
            // 所以下面的语句将引起异常的抛出
 
            Future future3 = es.submit(task3);
 
            System.out.println("task3:" + future3.get());
 
        } catch (Exception e){
 
            System.out.println(e.toString());
 
        }
 
       // 停止任务执行服务
 
        es.shutdownNow();
 
    }
 
}

打印结果:

task1: flag = 0

looping.

looping.

looping.

Interrupted

task2 cancel:true

java.util.concurrent.ExecutionException:java.lang.Exception: Bad flag value!


synchronized的不足

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

 

1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2、线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,很影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2、Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

 

在Java5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,用来控制对竞争资源并发访问的控制,这些内容主要集中在java.util.concurrent.locks包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。

Lock

Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chainlocking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected bythis lock
     } finally {
         l.unlock();
     }

锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。

Lock 实现提供了使用synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试(tryLock(long, TimeUnit))。

Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。

注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。

 

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class Test {
     
      //锁为类变量,这一点很重要
      //ReentrantLock的意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类
      private Lock lock = new ReentrantLock();
      private int counter = 0;
     
      public void add(){
             //获取锁
             lock.lock();
             try{
                    System.out.println(Thread.currentThread().getName()+"获取锁");
                    for(int i=0;i<10;i++){
                           counter = counter+i;
                    }
                    System.out.println("counter:"+counter);
             }finally{
                    //释放锁
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName()+"释放锁");
             }
            
      }
     
      public static void main(String[] args) {
            
             final Test test = new Test(); //不加final关键字,在下面的run()方法内是不能引用该实例的
            
             Runnable r = new Runnable(){
                    public void run(){
                           test.add();
                    }
             };
            
             Thread threadA = new Thread(r,"threadA");
             Thread threadB = new Thread(r,"threadB");
             threadA.start();
             threadB.start();
      }
 
}

打印结果:

threadA获取锁

counter:45

threadA释放锁

threadB获取锁

counter:90

threadB释放锁

 

Lock接口除了lock()方法,还有两种获取锁的方法:

tryLock()方法表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time,TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

ReadWriteLock

在上例中使用了Lock接口以及对象,使用它,很优雅的控制了竞争资源的安全访问,但是这种锁不区分读写,称这种锁为普通锁。为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,在一定程度上提高了程序的执行效率。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class Test {
     
     
      private ReadWriteLock lock = new ReentrantReadWriteLock();
      private int counter = 0;
     
      public void write(){
             //获取写入锁
             lock.writeLock();
            
             System.out.println(Thread.currentThread().getName()+"获取写入锁");
             for(int i=0;i<10;i++){
                    counter = counter + 1;
                    System.out.println(Thread.currentThread().getName()+"修改数据,counter:"+counter);
                    try {
                           Thread.sleep(500);
                    } catch(InterruptedException e) {
                           e.printStackTrace();
                    }
             }
            
            
             System.out.println(Thread.currentThread().getName()+"写入完毕");
      }
     
      public void read(){
             //获取读取锁
             lock.readLock();
     
             System.out.println(Thread.currentThread().getName()+"获取读出锁");
             for(int i=0;i<10;i++){
                    System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次读数据,counter:"+counter);
                    try {
                           Thread.sleep(200);
                    } catch(InterruptedException e) {
                           // TODOAuto-generated catch block
                           e.printStackTrace();
                    }
             }
             System.out.println(Thread.currentThread().getName()+"读取数据完毕");
      }
     
      public static void main(String[] args)throws InterruptedException {
            
             final Test test = new Test(); //不加final关键字,在下面的run()方法内是不能引用该实例的
            
             Runnable readRun = new Runnable(){
                    public void run(){
                           test.read();
                    }
             };
            
             Runnable writeRun = new Runnable(){
                    public void run(){
                           test.write();
                    }
             };
            
             Thread threadA = new Thread(writeRun,"threadA");
             Thread threadB = new Thread(readRun,"threadB");
             Thread threadC = new Thread(readRun,"threadC");
            
             //读与写同时开始
             threadA.start();
             threadB.start();
             threadC.start();
      }
 
}

打印结果:

threadA获取写入锁

threadA修改数据,counter:1

threadB获取读出锁

threadC获取读出锁

threadC第1次读数据,counter:1

threadB第1次读数据,counter:1

threadC第2次读数据,counter:1

threadB第2次读数据,counter:1

threadC第3次读数据,counter:1

threadB第3次读数据,counter:1

threadA修改数据,counter:2

threadC第4次读数据,counter:2

threadB第4次读数据,counter:2

threadC第5次读数据,counter:2

threadB第5次读数据,counter:2

threadC第6次读数据,counter:2

threadA修改数据,counter:3

threadB第6次读数据,counter:3

threadC第7次读数据,counter:3

threadB第7次读数据,counter:3

threadC第8次读数据,counter:3

threadB第8次读数据,counter:3

threadA修改数据,counter:4

threadC第9次读数据,counter:4

threadB第9次读数据,counter:4

threadC第10次读数据,counter:4

threadB第10次读数据,counter:4

threadB读取数据完毕

threadA修改数据,counter:5

threadC读取数据完毕

threadA修改数据,counter:6

threadA修改数据,counter:7

threadA修改数据,counter:8

threadA修改数据,counter:9

threadA修改数据,counter:10

threadA写入完毕

Condition

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了synchronized 方法和语句的使用,Condition替代了 Object 监视器方法的使用。

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得Condition 实例,使用其newCondition() 方法。

来看一下一个实例:

 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class Test {
        public static void main(String[] args){
                //创建并发访问的账户
                MyCount myCount = new MyCount("66666666666", 10000);
                //创建一个线程池
                ExecutorService pool =Executors.newFixedThreadPool(3);
                Thread t1 = new SaveThread("洪七公", myCount, 2000);
                Thread t2 = new SaveThread("黄老邪", myCount, 3600);
                Thread t3 = new DrawThread("欧阳锋", myCount, 2700);
                Thread t4 = new SaveThread("老顽童", myCount, 600);
                Thread t5 = new DrawThread("郭靖", myCount, 1300);
                Thread t6 = new DrawThread("黄蓉", myCount, 800);
                //执行各个线程
                pool.execute(t1);
                pool.execute(t2);
                pool.execute(t3);
                pool.execute(t4);
                pool.execute(t5);
                pool.execute(t6);
                //关闭线程池
                pool.shutdown();
        }
}
 
/**
* 存款线程类
*/
class SaveThread extends Thread {
        private String name;            //操作人
        private MyCount myCount;        //账户
        private int x;                  //存款金额
 
        SaveThread(String name, MyCount myCount, int x) {
                this.name = name;
                this.myCount = myCount;
                this.x = x;
        }
 
        public void run() {
                myCount.saving(x, name);
        }
}
 
/**
* 取款线程类
*/
class DrawThread extends Thread {
        private String name;            //操作人
        private MyCount myCount;        //账户
        private int x;                  //存款金额
 
        DrawThread(String name, MyCount myCount, int x) {
                this.name = name;
                this.myCount = myCount;
                this.x = x;
        }
 
        public void run() {
                myCount.drawing(x, name);
        }
}
 
 
/**
* 普通银行账户,不可透支
*/
class MyCount {
        private String oid;                     //账号
        private int cash;                       //账户余额
        private Lock lock =new ReentrantLock();//账户锁
        private Condition _save =lock.newCondition();    //存款条件
        private Condition _draw =lock.newCondition();    //取款条件
 
        MyCount(String oid, int cash) {
                this.oid = oid;
                this.cash = cash;
        }
 
        
        public void saving(int x, String name){
                lock.lock();                        //获取锁
                if (x > 0) {
                        cash += x;                    //存款
                        System.out.println(name+ "存款" + x +",当前余额为" + cash);
                }
                _draw.signalAll();            //唤醒所有等待线程。
                lock.unlock();                //释放锁
        }
 
       
        public void drawing(int x, String name){
                lock.lock();        //获取锁
                try {
                        if (cash - x < 0) {
                               _draw.await();    //如果存款为零,阻塞取款操作
                        } else {
                                cash -= x;         //取款
                               System.out.println(name + "取款" + x +",当前余额为"+ cash);
                        }
                        _save.signalAll();         //唤醒所有存款操作
                } catch (InterruptedException e) {
                        e.printStackTrace();
                } finally {
                        lock.unlock();                     //释放锁
                }
        }
}

打印结果:

洪七公存款2000,当前余额为12000

欧阳锋取款2700,当前余额为9300

郭靖取款1300,当前余额为8000

黄蓉取款800,当前余额为7200

黄老邪存款3600,当前余额为10800

老顽童存款600,当前余额为11400


原子量

所谓的原子量即操作变量的操作是“原子的”,该操作不可再分,因此是线程安全的。

多个线程对单个变量操作也会引起一些问题。如前面提到的类似i++这样的"读-改-写"复合操作(在一个操作序列中,后一个操作依赖前一次操作的结果),在多线程并发处理的时候会出现问题,因为可能一个线程修改了变量, 而另一个线程没有察觉到这样变化,当使用原子变量之后,则将一系列的复合操作合并为一个原子操作,从而避免这种问题(使用i.incrementAndGet()代替i++的操作)。

JDK5以后在java.util.concurrent.atomic包下提供了十几个原子类。常见的是 AtomicInteger,AtomicLong,AtomicReference以及它们 的数组形式,还有AtomicBoolean和为了处理 ABA问题引入的AtomicStampedReference类,最后就是基于反射的对volatile变量进行更新的 实用工具类:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater。这些原子类理论上能够大幅的提升性能。并且java.util.concurrent内的并发集合,线程池,执行器,同步器的内部实现大量的依赖这些无锁原子类,从而争取性能的最大化。

下面通过一个简单的例子看看:

import java.util.concurrent.atomic.AtomicInteger;
 
public class Test extends Thread{
   private AtomicCounter atomicCounter;
 
   public Test(AtomicCounter atomicCounter) {
     this.atomicCounter = atomicCounter;
   }
 
   @Override
   public void run() {
     long sleepTime = (long) (Math.random() *100);  //睡眠时间为随机值
     try {
       Thread.sleep(sleepTime);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     //使计数器增1
     atomicCounter.counterIncrement();
   }
 
   public static void main(String[] args)throws Exception {
     AtomicCounter atomicCounter = new AtomicCounter();
 
     for (int i = 0; i < 5000; i++) {  //开启5000个线程,共用一个AtomicCounter对象
       new Test(atomicCounter).start();
     }
 
     Thread.sleep(3000);
     //经过5000次的并发自增操作,打印结果应该为5000
     System.out.println("counter=" +atomicCounter.getCounter());
   }
}
 
class AtomicCounter {
      //原子更新的整型计数器
   private AtomicInteger counter = new AtomicInteger(0);
 
   public int getCounter() {
     return counter.get();
   }
 
   public void counterIncrement() {
     for (; ;) {
       //get():获取当前值
       int current = counter.get();
       int next = current + 1;
       //compareAndSet():如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
       //如果成功,则返回 true。返回 False 指示实际值与预期值不相等。
       //无限循环,直到取到预期值
       if (counter.compareAndSet(current,next))
         return;
     }
   }
}

打印结果:

counter=5000

跟预期的一样。

 

AtomicCounter内的共享变量使用了Integer的原子类代替,在get()方法中不使用锁,也不用担心获取的过程中别的线程去改变counter的值,因为这些原子类可以看成volatile的范化扩展,可见性能够保证。而在counterIncrement()方法中揭示了使用原子类的重要技巧:循环+CAS(Compare-And-Swap:一种实现无锁(lock-free)的非阻塞算法。在大多数处理器架构,包括IA32、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做)。这个技巧可以帮助我们实现复杂的非阻塞并发集合。方法中的counter.compareAndSet(current,next)就是原子类使用的精髓

在看另一个版本:

public class Test extends Thread{
   private AtomicCounter2 atomicCounter;
 
   public Test(AtomicCounter2 atomicCounter) {
     this.atomicCounter = atomicCounter;
   }
 
  @Override
   public void run() {
     long sleepTime = (long) (Math.random() *100);  //睡眠时间为随机值
     try {
       Thread.sleep(sleepTime);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     //使计数器增1
     atomicCounter.counterIncrement();
   }
 
   public static void main(String[] args)throws Exception {
     AtomicCounter2 atomicCounter = new AtomicCounter2();
 
     for (int i = 0; i < 5000; i++) {  //开启5000个线程,共用一个AtomicCounter对象
       new Test(atomicCounter).start();
     }
 
     Thread.sleep(3000);
     //经过5000次的并发自增操作,打印结果应该为5000
     System.out.println("counter=" +atomicCounter.getCounter());
   }
}
 
class AtomicCounter2 {
        //计数器只是用volatile关键字修饰,没有使用原子量
        private volatile int counter;
 
        public int getCounter() {
          return counter;
         }
 
        public int counterIncrement() {
             //自增操作在并发操作时会出现问题
         return counter++;
         }
      }


第一次运行结果:

counter=4970

第二次运行结果:

counter=4968

可见,这次的计数器出现了并发问题。我们预期打印结果为5000,实际上却小于5000。

虽然是对同一个变量进行了修改,但是变量的自增操作不是原子的,依然会出现问题。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而volatile 不能提供必须的原子特性。

比如,现在counter的值为2000,线程1对counter进行自增操作,执行第二步“修改”的时候,线程2也来取值并做修改,但是这时候线程1还没有把自增的结果存入counter变量,导致线程1与线程2取出的值都是2000,两个线程执行自增操作后,本应增至2002,但是实际上却只增加了1,变成2001。这就是为什么第二个例子打印的结果会小于5000。

下面我们使用原子量的概念对第二个例子进行修改,使之达到预期的效果。

将计数器类改为:

class AtomicCounter2 {
       
        private volatile int counter;
        //AtomicIntegerFieldUpdater:基于反射的实用工具,可以对指定类的指定volatile int 字段进行原子更新。
        //此类用于原子数据结构,该结构中同一节点的几个字段都独立受原子更新控制
        private static final AtomicIntegerFieldUpdater counterUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicCounter2.class,"counter");
 
        public int getCounter() {
          return counter;
         }
 
        public int counterIncrement() {
             //以原子方式将此更新器管理的给定对象的当前值加 1。
         return counterUpdater.getAndIncrement(this);
         }
      }

打印结果:

counter=5000

修改后的计数器内有个volatile的共享变量counter,并且有个类变量counterUpdater作为 counter的更新器。而counterUpdater.getAndIncrement(this)的内部实现其实和第一个例子中几乎一样。不同的是通过反射找到要原子操作更新的变量counter,但是“循环+CAS”的精髓是一样的。

 

特别需要注意:原子变量只能保证对一个变量的操作是原子的,如果有多个原子变量之间存在依赖的复合操作,也不可能是安全的。另外一种情况是要将更多的复合操作作为一个原子操作,则需要使用synchronized将要作为原子操作的语句包围起来。因为涉及到可变的共享变量(类实例成员变量)才会涉及到同步,否则不必使用synchronized。

 

 

转载于:https://www.cnblogs.com/duadu/p/6335814.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值