Java并发编程(二)同步

如果你的java基础较弱,或者不大了解java多线程请先看这篇文章java多线程(一)线程定义、状态和属性

同步一直是java多线程的难点,在我们做android开发时也很少应用,但这并不是我们不熟悉同步的理由。希望这篇文章能使更多的人能够了解并且应用java的同步。
在多线程的应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常成为竞争条件。
竞争条件最容易理解的例子就是:比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。并且无法保证其原子性,如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。解决方法为,当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后在把锁给另一个要用这个资源的线程。这样就不会出现上述情况。

1. 锁对象
synchronized关键字自动提供了锁以及相关的条件,大多数需要显式锁的情况使用synchronized非常的方便,但是等我们了解ReentrantLock类和条件对象时,我们能更好的理解synchronized关键字。ReentrantLock是JAVA SE 5.0引入的, 用ReentrantLock保护代码块的结构如下:

mLock.lock();
try{
...
}
finally{
mLock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们则被阻塞直到第一个线程释放锁对象。把解锁的操作放在finally中是十分必要的,如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远阻塞。

2. 条件对象
进入临界区时,却发现在某一个条件满足之后,它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又称作条件变量。
我们来看看下面的例子来看看为何需要条件对象

假设一个场景我们需要用银行转账,我们首先写了银行的类,它的构造函数需要传入账户数量和账户金额

public class Bank {
private double[] accounts;
    private Lock bankLock;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    }

接下来我们要提款,写一个提款的方法,from是转账方,to是接收方,amount转账金额,结果我们发现转账方余额不足,如果有其他线程给这个转账方再存足够的钱就可以转账成功了,但是这个线程已经获取了锁,它具有排他性,别的线程也无法获取锁来进行存款操作,这就是我们需要引入条件对象的原因。

public void transfer(int from,int to,int amount){
     bankLock.lock();
     try{
         while (accounts[from]<amount){
             //wait
         }
     }finally {
         bankLock.unlock();
     }
 }

一个锁对象拥有多个相关的条件对象,可以用newCondition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁

public class Bank {
private double[] accounts;
    private Lock bankLock;
    private Condition condition;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        //得到条件对象
        condition=bankLock.newCondition();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount) throws InterruptedException {
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //阻塞当前线程,并放弃锁
                condition.await();
            }
        }finally {
            bankLock.unlock();
        }
    }
}

等待获得锁的线程和调用await方法的线程本质上是不同的,一旦一个线程调用的await方法,他就会进入该条件的等待集。当锁可用时,该线程不能马上解锁,相反他处于阻塞状态,直到另一个线程调用了同一个条件上的signalAll方法时为止。当另一个线程准备转账给我们此前的转账方时,只要调用condition.signalAll();该调用会重新激活因为这一条件而等待的所有线程。
当一个线程调用了await方法他没法重新激活自身,并寄希望于其他线程来调用signalAll方法来激活自身,如果没有其他线程来激活等待的线程,那么就会产生死锁现象,如果所有的其他线程都被阻塞,最后一个活动线程在解除其他线程阻塞状态前调用await,那么它也被阻塞,就没有任何线程可以解除其他线程的阻塞,程序就被挂起了。
那何时调用signalAll呢?正常来说应该是有利于等待线程的方向改变时来调用signalAll。在这个例子里就是,当一个账户余额发生变化时,等待的线程应该有机会检查余额。

public void transfer(int from,int to,int amount) throws InterruptedException {
       bankLock.lock();
       try{
           while (accounts[from]<amount){
               //阻塞当前线程,并放弃锁
               condition.await();
           }
           //转账的操作
           ...
           condition.signalAll();
       }finally {
           bankLock.unlock();
       }
   }

当调用signalAll方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。还有一个方法是signal,它则是随机解除某个线程的阻塞,如果该线程仍然不能运行,那么则再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了。

3. Synchronized关键字
Lock和Condition接口为程序设计人员提供了高度的锁定控制,然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到java语言内部的机制。从Java1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
换句话说,

public synchronized void method(){

}

等价于

public void method(){
this.lock.lock();
try{

}finally{
this.lock.unlock();
}

上面银行的例子,我们可以将Bank类的transfer方法声明为synchronized,而不是使用一个显示的锁。
内部对象锁只有一个相关条件,wait方法添加到一个线程到等待集中,notifyAll或者notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于condition.signalAll();

我们上面的例子transfer方法也可以这样写:

public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
    while (accounts[from]<amount) {
        wait();
    }
    //转账的操作
    ...
    notifyAll();   
    }

可以看到使用synchronized关键字来编写代码要简洁很多,当然要理解这一代码,你必须要了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。

4. 同步阻塞
上面我们说过,每一个Java对象都有一个锁,线程可以调用同步方法来获得锁,还有另一种机制可以获得锁,通过进入一个同步阻塞,当线程进入如下形式的阻塞:

synchronized(obj){

}

于是他获得了obj的锁。再来看看Bank类

public class Bank {
private double[] accounts;
private Object lock=new Object();
   public Bank(int n,double initialBalance){
        accounts=new double[n];
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount){
        synchronized(lock){
          //转账的操作
            ...
        }
    }
}

在此,lock对象创建仅仅是用来使用每个Java对象持有的锁。有时开发人员使用一个对象的锁来实现额外的原子操作,称为客户端锁定。例如Vector类,它的方法是同步的。现在假设在Vector中存储银行余额

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
}

Vecror类的get和set方法是同步的,但是这并未对我们有所帮助。在第一次对get调用完成以后,一个线程完全可能在transfer方法中被被剥夺运行权,于是另一个线程可能在相同的存储位置存入了不同的值,但是,我们可以截获这个锁

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  synchronized(accounts){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
  }
}

客户端锁定(同步代码块)是非常脆弱的,通常不推荐使用,一般实现同步最好用java.util.concurrent包下提供的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量的使用同步方法,他可以减少编写代码的数量,减少出错的几率,如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition。

分享到 评论

Android5.x Toolbar和Palette应用解析

本文首发于微信公众号「后厂村码农」

查看更多

分享到 评论

Android5.x Notification应用解析

本文首发于微信公众号「后厂村码农」

查看更多

分享到 评论

Android5.x CardView 应用解析

本文首发于微信公众号「后厂村码农」

查看更多

分享到 评论

Android5.x RecyclerView 应用解析

本文首发于微信公众号「后厂村码农」

查看更多

分享到 评论

Java并发编程(三)volatile域

前言

有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大,volatile关键字为实例域的同步访问提供了免锁的机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。再讲到volatile关键字之前我们需要了解一下内存模型的相关概念以及并发编程中的三个特性:原子性,可见性和有序性。

查看更多

分享到 评论

Java并发编程(一)线程定义、状态和属性

一 、线程和进程

1. 什么是线程和进程的区别:
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在java语言中,线程有四种状态:运行 、就绪、挂起和结束。
进程是指一段正在执行的程序。而线程有时也被成为轻量级的进程,他是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内功空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程都拥有自己的棧空间。
2. 为何要使用多进程
在操作系统级别上来看主要有以下几个方面:

  • 使用多线程可以减少程序的响应时间,如果某个操作和耗时,或者陷入长时间的等待,此时程序讲不会响应鼠标和键盘等的操作,使用多线程后可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
  • 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
  • 多CPU或者多核计算机本身就具备执行多线程的能力,如果使用单个进程,将无法重复利用计算机资源,造成资源的巨大浪费。在多CPU计算机使用多线程能提高CPU的利用率。
  • 使用多线程能简化程序的结构,使程序便于理解和维护

二、创建线程
多线程的实现一般有以下三种方法其中前两种为最常用的方法:
1. 继承Thread类,重写run()方法
Thread本质上也是实现了Runnable接口的一个实例。需要注意的是调用start()方法后并不是是立即的执行多线程的代码,而是使该线程变为可运行态,什么时候运行多线程代码是由操作系统决定的。
以下是主要步骤:
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。

public class TestThread extends Thread{ 
    public void run() {
            System.out.println("Hello World");
        }  
    public static void main(String[] args) {
        Thread mThread = new TestThread();
        mThread.start(); 
    } 
}

2. 实现Runnable接口,并实现该接口的run()方法
以下是主要步骤:
(1)自定义类并实现Runnable接口,实现run()方法。
(2)创建Thread子类的实例,用实现Runnable接口的对象作为参数实例化该Thread对象。
(3)调用Thread的start()方法来启动该线程。

public class TestRunnable implements Runnable {
    public void run() { 
            System.out.println("Hello World");
        } 
}

public class TestRunnable {
    public static void main(String[] args) {
        TestRunnable mTestRunnable = new TestRunnable();      
        Thread mThread = new Thread(mTestRunnable);
        mThread.start(); 
    } 
}

3. 实现Callable接口,重写call()方法
Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为以下的3点:
(1)Callable可以在任务接受后提供一个返回值,Runnable无法提供这个功能。
(2)Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。
(3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,他提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下就可以使用Future来监视目标线程调用call()方法的情况,但调用Future的get()方法以获取结果时,当前线程就会阻塞,直到call()方法的返回结果。

public class TestCallable {  
    //创建线程类
    public static class MyTestCallable  implements Callable {  
        public String call() throws Exception {  
             retun "Hello World";
            }  
        }  
public static void main(String[] args) {  
        MyTestCallable mMyTestCallable= new MyTestCallable();  
        ExecutorService mExecutorService = Executors.newSingleThreadPool();  
        Future mfuture = mExecutorService.submit(mMyTestCallable);  
        try { 
        //等待线程结束,并返回结果
            System.out.println(mfuture.get());  
        } catch (Exception e) {  
           e.printStackTrace();
        } 
    }  
}

上述程序的输出结果为:Hello World

在这三种方式中,一般推荐实现Runnable接口的方式,其原因是:首先,Thread类定义了多种方法可以被派生类使用重写,但是只有run()方法是必须被重写的,实现这个线程的主要功能,这也是实现Runnable接口需要的方法。其次,一个类应该在他们需要加强或者修改时才会被继承。因此如果没有必要重写Thread类的其他方法,那么在这种情况下最好是用实现Runnable接口的方式。

三、中断线程
当线程的run()方法执行方法体中的最后一条语句后,并经由执行return语句返回时,或者出现在方法中没有捕获的异常时线程将终止。在java早期版本中有一个stop方法,其他线程可以调用它终止线程,但是这个方法现在已经被弃用了。
interrupt方法可以用来请求终止线程,当一个线程调用interrupt方法时,线程的中断状态将被置位。这是每个线程都具有的boolean标志,每个线程都应该不时的检查这个标志,来判断线程是否被中断。
要想弄清线程是否被置位,可以调用Thread.currentThread().isInterrupted():

while(!Thread.currentThread().isInterrupted()){
do something
}

但是如果一个线程被阻塞,就无法检测中断状态。这是产生InterruptedException的地方。当一个被阻塞的线程(调用sleep或者wait)上调用interrupt方法。阻塞调用将会被InterruptedException中断。
如果每次迭代之后都调用sleep方法(或者其他可中断的方法),isInterrupted检测就没必要也没用处了,如果在中断状态被置位时调用sleep方法,它不会休眠反而会清除这一状态并抛出InterruptedException。所以如果在循环中调用sleep,不要去检测中断状态,只需捕获InterruptedException。
在很多发布的代码中会发现InterruptedException被抑制在很低的层次上:

void myTask(){
...
try{
sleep(50)
}catch(InterruptedException e){
...
}
}

不要这样做,如果不认为catch中做一处理有什么好处的话,有两种合理的选择:

  • 在catch中调用Thread.currentThread().interrup()来设置中断状态。调用者可以对其进行检测
  • 更好的选择用throw InterruptedException标记你的方法,不采用try语句块来捕获已成。这样调用者可以捕获这个异常:
void myTask()throw InterruptedException{
sleep(50)
}

四、线程的状态

(1). 新建状态(New):新创建了一个线程对象。
(2). 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
(3). 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
(4). 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    (5). 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    这里写图片描述

五、线程的优先级和守护线程

1. 线程优先级
在java中,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以用setPriority方法提高或降低任何一个线程优先级。可以将优先级设置在MIN_PRIORITY(在Thread类定义为1)与MAX_PRIORITY(在Thread类定义为10)之间的任何值。线程的默认优先级为NORM_PRIORITY(在Thread类定义为5)。
尽量不要依赖优先级,如果确实要用,应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态,低优先级线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有搞优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死。

2. 守护线程

调用setDaemon(true);将线程转换为守护线程。守护线程唯一的用途就是为其他线程提供服务。计时线程就是一个例子,他定时发送信号给其他线程或者清空过时的告诉缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
另外JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。

分享到 评论