Fork me on GitHub

Java并发笔记(二)

1. 活跃性危险

  • 死锁(最常见)
  • 饥饿

    当线程由于无法访问它所需的资源而不能继续执行时,就发生了饥饿。引发饥饿最常见资源就是CPU时钟周期

  • 活锁

    活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 

    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

    活锁可以认为是一种特殊的饥饿。 

    下面这个例子在有的文章里面认为是活锁。实际上这只是一种饥饿。因为没有体现出“活”的特点:

      如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。

      然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......T2可能永远等待。

      (假设事务T2再不断的重复尝试获取锁R,那么这个就是活锁。)

  •  丢失信号

2. 锁的相关概念

  • 可重入锁

  Java提供了强制原子性的内部锁机制:synchronized块。但是内部锁是可重入的,当线程试图获得它自己占有的锁时,请求会成功。ReentrantLock也是可重入锁

  简单的说,就是在一个synchronized方法内部调用本类(或父类)的其他synchronized方法时,永远可以拿到锁。

public class LoggingWidget extends Widget{  
  public static void main(String[] args) {  
              LoggingWidget lw=new LoggingWidget();  
              lw.doSomething();  
  }  
  public synchronized void doSomething(){  
              System.out.println("LoggingWidget->doSomething()");  
              doAnotherThing();         //调用自己类中其他的synchronized方法  
              super.doSomething();   //调用父类的synchronized方法  
  }  
  private synchronized void doAnotherThing(){  
              System.out.println("LoggingWidget->doAnotherThing()");  
  }  
}  
class Widget{  
  public synchronized void doSomething(){  
              System.out.println("Widget->doSomething()");  
  }  
}  

  执行结果是:

LoggingWidget->doSomething()  
LoggingWidget->doAnotherThing()  
Widget->doSomething() 

  可见,在java内部,调用父类的synchronized方法和调用自己类中其他synchronized方法都不会阻碍该程序的运行。

  正是因为java线程是基于“每线程(per-thread)”,而不是基于“每调用的(per-invocation)”的。

  重进入的实现是通过为每个锁关联一个请求计数和一个占有它的线程。

 

  • 可中断锁

  可中断锁:顾名思义,就是可以相应中断的锁。

  在Java中,synchronized就不是可中断锁,而Lock是可中断锁

  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

  

  • 公平锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

  而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

 

  • 读写锁

  读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

  正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

  ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

  可以通过readLock()获取读锁,通过writeLock()获取写锁。

 

3. Lock (http://www.cnblogs.com/dolphin0520/p/3923167.html

  synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

  如果一个代码块被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则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

  

  Lock接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

  使用形式:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

 

  ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。例子:

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

   

  ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

 

   一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

  ReentrantReadWriteLock实现了ReadWriteLock接口。

   

  总结来说,Lock和synchronized有以下几点不同:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  

4. 线程生命周期

  当我们在Java程序中新建一个线程时,它的状态是New。当我们调用线程的start()方法时,状态被改变为Runnable。线程调度器会为Runnable线程池中的线程分配CPU时间并且将它们的状态改变为Running。其他的线程状态还有Waiting/Blocked 和Dead

  

  • Runnable(就绪)状态

  处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。

  (tip)如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

  • Running(运行)状态

  处于Runnable(就绪)状态的线程,如果获得了cpu的调度,就会从Runnable状态变为Running状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从Running状态变为Runnable状态。重新等待系统分配资源。 

  当发生如下情况是,线程会从运行状态变为阻塞状态:

  ①、线程调用sleep方法主动放弃所占用的系统资源

  ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

  ③、线程试图获得一个同步监视器(锁),但更改同步监视器正被其他线程所持有

  ④、线程在等待某个通知(notify

  ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

  当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

  

  • Waiting/Blocked(阻塞)状态

  处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 

  在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

  有三种方法可以暂停Threads执行。

  (1) sleep()

  sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
}  

 

  Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。 

  (2) yield()

  线程让步方法。yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。

  但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

  (3) join()

  线程合并方法。就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
        thread.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度  
        for(int i=0;i<30;i++){  
            System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!");  
        }  
    }  
}  
  
class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
        }  
    }  
}

   在这个例子中,在主线程中调用thread.join(); 就是将主线程加入到thread子线程后面等待执行。不过有时间限制,为1毫秒。

 5. wait(), notify()和notifyAll()

  • 为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

   因为Java的每个对象中都有一个锁。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。

  • 为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?

   当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

  

 

 

附:JAVA多线程和并发基础面试问答

  http://www.cnblogs.com/dolphin0520/p/3932934.html

  • 为什么Thread类的sleep()和yield()方法是静态的?

  Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 

  • 如何确保线程安全?

  在Java中可以有很多方法来保证线程安全——同步,使用原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类。

 

 

 

 

 

 

 

posted @ 2015-04-25 22:14  落崖惊风  阅读(244)  评论(0编辑  收藏  举报