Java线程和锁

名词解释

  Java平台中的Thread对象,Runnable对象均可以产生线程对象,线程对象即可以产生线程的对象。线程,是指正在执行的一个指点令序列。
  此处穿插一下线程和进程的区别:线程是进程更小的一个划分尺度,程序运行时必产生一个进程,此进程必产生至少一个线程,每个进程都会有自己独立的内存单元,而进程中的线程则是共享这块内存单元。简而言之,可以这样区分:

  1. 进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位);
  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
    而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  4. 多进程程序更健壮,一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间,而多线程程序只要有一个线程死掉,整个进程也死掉了。

创建线程

  我们再次回到线程的操作中,创建线程有以下几种方式:

  1. 继承Thread类创建线程类
    通过继承Thread类创建线程类的具体步骤和具体代码如下:
    • 定义一个继承Thread类的子类,并重写该类的run()方法;
    • 创建Thread子类的实例,即创建了线程对象;
    • 调用该线程对象的start()方法启动线程。
 class SomeThread extends Thread{ 
@Override    
public void run()   { 
     //do something here  
    }  
 } 
 
public static void main(String[] args){
 SomeThread oneThread = new SomeThread();   
 oneThread.start(); 
}
  1. 实现Runnable接口创建线程类
    通过实现Runnable接口创建线程类的具体步骤和具体代码如下:
    • 定义Runnable接口的实现类,并重写该接口的run()方法;
    • 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
class SomeRunnable implements Runnable   { 
@Override  
public void run()   { 
  //do something here  
  }  
} 
Runnable oneRunnable = new SomeRunnable();   
Thread oneThread = new Thread(oneRunnable);   
oneThread.start();

  上述两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。

  1. 通过Callable和Future创建线程
    通过Callable和Future创建线程的具体步骤和具体代码如下:
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:
public interface Callable   { 
  V call() throws Exception;  
 } 
  步骤1:创建实现Callable接口的类SomeCallable(略);   
  步骤2:创建一个类对象: 
      Callable oneCallable = new SomeCallable(); 
  步骤3:由Callable创建一个FutureTask对象:   
    FutureTask oneTask = new FutureTask(oneCallable); 
  注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。 
  步骤4:由FutureTask创建一个Thread对象:   
    Thread oneThread = new Thread(oneTask);   
  步骤5:启动线程:  
    oneThread.start();

线程状态

   线程之间的转态转换如下图:


image.png
  1. 新建状态
      用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
      注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。
  2. 就绪状态
      处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
      提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
  3. 运行状态
      处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
      处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
    注: 当发生如下情况是,线程会从运行状态变为阻塞状态:
    ①、线程调用sleep方法主动放弃所占用的系统资源
    ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
    ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
    ④、线程在等待某个通知(notify)
    ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁, 所以程序应该尽量避免使用该方法。
      当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
  4. 阻塞状态
       处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
      在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
    补充线程sleep()方法和wait()方法的区别:
      sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
    sleep()和yield()的区别:
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
  5. 死亡状态
      当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

线程方法

  线程同步和线程调度的方法如下:

  1. wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  3. notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  4. notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
  5. join(): 当前线程等该加入该线程后面,等待该线程终止。
  6. join(long millis) :当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
  7. join(long millis,int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
  8. setPriority(int newPriority):设置线程优先级,MAX_PRIORITY =10;MIN_PRIORITY =1;NORM_PRIORITY =5
  9. getPriority():返回线程优先级
    注: 虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
  10. setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
    参数:
     on - 如果为 true,则将该线程标记为守护线程。
     抛出:
     IllegalThreadStateException - 如果该线程处于活动状态。
     SecurityException - 如果当前线程无法修改该线程。
    守护线程说明:
      守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
      守护线程的用途为:
    • 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
    • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

线程同步

1、synchronized关键字
  synchronized会为锁住的对象加锁,当线程访问时需要申请锁,申请不到是处于阻塞状态,可以用于锁住方法或者锁住类,当synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。注意以下实例:

// 第一种
synchronized(this) {
      ...
}
// 第二种
synchronized(A.class) {
    ...
}

a. 第一种方式表示:当前同步代码块中锁对象是当前类的一个实例,当线程访问同一个实例锁定的同步方法时,将会被禁止。
b. 第二种方式表示:当前同步代码块中锁对象对当前类的任意实例均有效,即任意线程访问该代码块都会被禁止。
锁住代码块的意义在于:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
2、volatile关键字
  volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
3、Lock锁
  Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
  ReenreantLock类的常用方法有:

 ReentrantLock() : 创建一个ReentrantLock实例         
 lock() : 获得锁,如果锁已被其他线程获取,则进行等待。  
 unlock() : 释放锁void lockInterruptibly() throws InterruptedException:lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。boolean tryLock():表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。boolean tryLock(long time, TimeUnit unit) throws InterruptedException:这个方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
Condition newCondition():
isFair():判断锁是否是公平锁
isLocked():判断锁是否被任何线程获取了
isHeldByCurrentThread() :判断锁是否被当前线程获取了
hasQueuedThreads():判断是否有线程在等待该锁

注:可重入锁的概念简单说明,可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。即当前线程调用synchronized修饰的methondA,A又调用了synchronized的方法B,此时自动获取锁,不需要重新申请。
&参考博客:https://www.jianshu.com/p/e25983256448

阻塞队列

  阻塞队列(BlockingQueue):BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
  BlockingQueue提供如下两个支持阻塞的方法:
(1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
  BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
(2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
BlockingQueue实现类:

ArrayBlockingQueue :基于数组实现的BlockingQueue队列。
LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

  这一节只总结线程和锁,下一节总结线程池。

你可能感兴趣的:(Java线程和锁)