Java多线程、锁、异常处理等(2019.4.9)

本文详细探讨了Java中的多线程创建方式,包括继承Thread类、实现Runnable接口以及使用Callable和Future,同时分析了线程状态转化、锁的类型(如可重入锁、公平锁与非公平锁)、线程池的概念,以及异常处理机制。此外,还提到了线程间的通信和线程同步方法,如wait/notify、join方法,强调了volatile关键字在多线程中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

       本博客为个人复习时总结用,无商业目的,其大多数内容皆为博主整理所得,并非原创。侵删。


线程创建

继承Thread类创建线程类
  1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

  2. 创建Thread子类的实例,即创建了线程对象。

  3. 调用线程对象的start()方法来启动该线程。

     public class FirstThreadTest extends Thread{  
         int i = 0;  
         //重写run方法,run方法的方法体就是现场执行体  
         public void run()  
         {  
             for(;i<100;i++){  
             System.out.println(getName()+"  "+i);  
             }  
         }  
         public static void main(String[] args)  
         {  
             for(int i = 0;i< 100;i++)  
             {  
                 System.out.println(Thread.currentThread().getName()+"  : "+i);  
                 if(i==20)  
                 {  
                     new FirstThreadTest().start();  
                     new FirstThreadTest().start();  
                 }  
             }  
         }  
     } 
    

        上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。

通过Runnable接口创建线程类
  1. 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

  3. 调用线程对象的start()方法来启动该线程。

     public class RunnableThreadTest implements Runnable  
     {  
       
         private int i;  
         public void run()  
         {  
             for(i = 0;i <100;i++)  
             {  
                 System.out.println(Thread.currentThread().getName()+" "+i);  
             }  
         }  
         public static void main(String[] args)  
         {  
             for(int i = 0;i < 100;i++)  
             {  
                 System.out.println(Thread.currentThread().getName()+" "+i);  
                 if(i==20)  
                 {  
                     RunnableThreadTest rtt = new RunnableThreadTest();  
                     new Thread(rtt,"新线程1").start();  
                     new Thread(rtt,"新线程2").start();  
                 }  
             }  
         }   
     }  
    

        线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run();方法,该方法执行完成后,线程就消亡了。

通过Callable和Future创建线程
  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

     public interface Callable
     {
       V call() throws Exception;
     }
    
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)

  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。

  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

     import java.util.concurrent.Callable;  
     import java.util.concurrent.ExecutionException;  
     import java.util.concurrent.FutureTask;  
       
     public class CallableThreadTest implements Callable<Integer>  
     {  
       
         public static void main(String[] args)  
         {  
             CallableThreadTest ctt = new CallableThreadTest();  
             FutureTask<Integer> ft = new FutureTask<>(ctt);  
             for(int i = 0;i < 100;i++)  
             {  
                 System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
                 if(i==20)  
                 {  
                     new Thread(ft,"有返回值的线程").start();  
                 }  
             }  
             try  
             {  
                 System.out.println("子线程的返回值:"+ft.get());  
             } catch (InterruptedException e)  
             {  
                 e.printStackTrace();  
             } catch (ExecutionException e)  
             {  
                 e.printStackTrace();  
             }  
       
         }  
       
         @Override  
         public Integer call() throws Exception  
         {  
             int i = 0;  
             for(;i<100;i++)  
             {  
                 System.out.println(Thread.currentThread().getName()+" "+i);  
             }  
             return i;  
         }  
       
     }  
    
创建线程的三种方式的对比
  • 采用实现Runnable、Callable接口的方式创建多线程时,
    优势是:
            线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
            在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:
           编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

  • 使用继承Thread类的方式创建多线程时,
    优势是:
           编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:
           线程类已经继承了Thread类,所以不能再继承其他父类。

  • Runnable和Callable的区别

  1. Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
  2. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  3. call方法可以抛出异常,run方法不可以。
  4. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

线程的状态转化关系

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
      -等待阻塞(WAITING):运行的线程执行wait()方法,JVM会把该线程放入等待池中。
      - 同步阻塞(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
      - 超时阻塞(TIME_WAITING):运行的线程执行sleep(long)或join(long)方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程状态转换图 1
线程转换图 2
       图中的方法解析如下:
       Thread.sleep():在指定时间内让当前正在执行的线程暂停执行,但不会释放"锁标志"。不推荐使用。
       Thread.sleep(long):使当前线程进入阻塞状态,在指定时间内不会执行。
       Object.wait()和Object.wait(long):在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的"锁标志",从而使别的线程有机会抢占该锁。 当前线程必须拥有当前对象锁,如果当前线程不是此锁的拥有者,会抛IllegalMonitorStateException异常。 唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常,wait()和notify()必须在synchronized函数或synchronized中进行调用。如果在non-synchronized函数或non-synchronized中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
       Object.notifyAll():从对象等待池中唤醒所有等待等待线程
       Object.notify():从对象等待池中唤醒其中一个线程。

//使用示例:
public class NotifyTest {  
    private String flag[] = { "true" };  //不使用其他基本类型原因:避免对象改变,从而影响到对象锁的判断。
  
    class NotifyThread extends Thread {  
        public NotifyThread(String name) {  
            super(name);  //可以继承父类属性
        }  
  
        public void run() {  
            try {  
                sleep(3000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            synchronized (flag) {  //一定要对对象增加同步锁
                flag[0] = "false";  
                flag.notifyAll();  
            }  
        }  
    };  
  
    class WaitThread extends Thread {  
        public WaitThread(String name) {  
            super(name);  
        }  
  
        public void run() {  
            synchronized (flag) {  
                while (flag[0] != "false") {  
                    System.out.println(getName() + " begin waiting!");  
                    long waitTime = System.currentTimeMillis();  
                    try {  
                        flag.wait();  //写在while中,对flag有监听
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                    waitTime = System.currentTimeMillis() - waitTime;  
                    System.out.println("wait time :" + waitTime);  
                }  
                System.out.println(getName() + " end waiting!");  
            }  
        }  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println("Main Thread Run!");  
        NotifyTest test = new NotifyTest();  
        NotifyThread notifyThread = test.new NotifyThread("notify01");  
        WaitThread waitThread01 = test.new WaitThread("waiter01");  
        WaitThread waitThread02 = test.new WaitThread("waiter02");  
        WaitThread waitThread03 = test.new WaitThread("waiter03");  
        notifyThread.start();  
        waitThread01.start();  
        waitThread02.start();  
        waitThread03.start();  
    }  
  
}  

       Thread.yield():暂停当前正在执行的线程对象,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,yield()只能使同优先级或更高优先级的线程有执行的机会。

       在run内部使用

       Thread.Join():把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

       详见 T1、T2、T3线程题(后面有写)。


Java 锁类型(重要)

总结:

       可重入锁代表如果获取到当前对象,则不需要继续获取当前对象。reentrantlock
       可中断锁:可以使用中断
       lock.lockInterruptibly();//等待锁的过程中会立即响应中断 对比 lock.lock

实现方式:
  1. synchronize
           Lock有三个实现类,一个是ReentrantLock ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

  2. 公平锁与非公平锁
           公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。
           对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
           对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

  3. 可重入锁

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功

       如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

class MyClass {
    public synchronized void method1() {
        method2();
    }
 
    public synchronized void method2() {
 
    }
} 

       上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
       而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

  1. 可中断锁
           可中断锁:顾名思义,就是可以相应中断的锁。
           在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
    如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
           lockInterruptibly()的用法时已经体现了Lock的可中断性。

           lockInterruptibly():
           ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

     class MyThread05 extends Thread{
     	public void test3() throws Exception{
     	    final Lock lock=new ReentrantLock();
     	    lock.lock();
     	    Thread.sleep(1000);
     	    Thread t1=new Thread(new Runnable(){
     	        @Override
     	        public void run() {
     	            lock.lock();
     //	        	try {
     //					lock.lockInterruptibly();
     //				} catch (InterruptedException e) {
     //					// TODO Auto-generated catch block
     //					e.printStackTrace();
     //				}
     	            System.out.println(Thread.currentThread().getName()+" interrupted.");
     	        }
     	    });
     	    t1.start();
     	    Thread.sleep(1000);
     	    t1.interrupt();
     	    Thread.sleep(1000000);
     	}
     }
    
  2. 独享锁/共享锁

  • 独享锁是指该锁一次只能被一个线程所持有。
  • 共享锁是指该锁可被多个线程所持有.
  • 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  • 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
  • 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
  • 对于Synchronized而言,当然是独享锁。
  1. 读写锁
  • 读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
  • 正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
  • ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
    可以通过readLock()获取读锁,通过writeLock()获取写锁。

守护线程以及线程优先级

守护线程:

       首先,我们可以通过t.setDaemon(true)的方法将线程转化为守护线程。而守护线程的唯一作用就是为其他线程提供服务。计时线程就是一个典型的例子,它定时地发送“计时器滴答”信号告诉其他线程去执行某项任务。当只剩下守护线程时,虚拟机就退出了,因为如果只剩下守护线程,程序就没有必要执行了。另外JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。最后还有一点需要特别注意的是在java虚拟机退出时Daemon线程中的finally代码块并不一定会执行,代码示例:

public class Demon {  
    public static void main(String[] args) {  
        Thread deamon = new Thread(new DaemonRunner(),"DaemonRunner");  
        //设置为守护线程  
        deamon.setDaemon(true);  
        deamon.start();//启动线程  
    }  
           
    static class DaemonRunner implements Runnable{  
        @Override  
        public void run() {  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }finally{  
                System.out.println("这里的代码在java虚拟机退出时并不一定会执行哦!");  
            }  
        }  
    }  
}  

       因此在构建Daemon线程时,不能依靠finally代码块中的内容来确保执行关闭或清理资源的逻辑。

线程的优先级:

       在现代操作系统中基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。在java线程中,通过一个整型的成员变量Priority来控制线程优先级。

       每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以用setPriority方法提高或降低任何一个线程优先级。可以将优先级设置在MIN_PRIORITY(在Thread类定义为1)与MAX_PRIORITY(在Thread类定义为10)之间的任何值。线程的默认优先级为NORM_PRIORITY(在Thread类定义为5)。


在多线程并发访问一个共享变量时

       为了保证逻辑的正确

  • 加锁,性能最低,能保证原子性、可见性,防止指令重排;
  • volatile修饰,性能中等,能保证可见性,防止指令重排;
  • 使用getObjectVolatile,性能最好,可防止指令重排;

并发控制代码

  1. 阻塞队列实现方式:

     public class BlockQueue{
      private List list = new ArrayList();
     
      public synchronized Object pop() throws InterruptedException{
      while (list.size()==0){
      this.wait();
      }
      if (list.size()>0){
      	return list.remove(0);
      } else{
      	return null;
      }
      }
     	
     	 public synchronized Object put(Object obj){
     	 list.add(obj);
     	 this.notify();
     	 }
     	 
     }
    
  2. 线程间通信方式
           https://www.cnblogs.com/hapjin/p/5492619.html

  3. 同步:
           这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。
           由于线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了 通信。
           这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

  4. while轮询的方式
           通过共享变量进行while轮询检测。
           注意:此时对该对象不能进行同步锁操作,因为会造成while轮询占用锁,其他获取不到信息。

  5. wait/notify机制
           线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。

           这里用到了Object类的 wait() 和 notify() 方法。
           当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。
           当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。

           这种方式的一个好处就是CPU的利用率提高了。

           但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。

  6. 管道通信
           管道通信就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

           具体就不介绍了。分布式系统中说的两种通信机制:共享内存机制和消息通信机制。感觉前面的①中的synchronized关键字和②中的while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现进程间的交流。

           而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。
    https://www.cnblogs.com/huzi007/p/7080393.html


进程间通信

  1. 无名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
  3. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  4. 消息队列( message queue ): 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  7. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  8. 套接字( socket ): 套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
想要更深入了解,建议看一下join的源码,也很简单的,使用wait方法实现的。

public static void main(String[] args) {
        method01();
        method02();
    }

/**
 * 第一种实现方式,顺序写死在线程代码的内部了,有时候不方便
 */
private static void method01() {
    Thread t1 = new Thread(new Runnable() {
        @Override public void run() {
            System.out.println("t1 is finished");
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override public void run() {
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 is finished");
        }
    });
    Thread t3 = new Thread(new Runnable() {
        @Override public void run() {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3 is finished");
        }
    });

    t3.start();
    t2.start();
    t1.start();
}


/**
 * 第二种实现方式,线程执行顺序可以在方法中调换
 */
private static void method02(){
    Runnable runnable = new Runnable() {
        @Override public void run() {
            System.out.println(Thread.currentThread().getName() + "执行完成");
        }
    };
    Thread t1 = new Thread(runnable, "t1");
    Thread t2 = new Thread(runnable, "t2");
    Thread t3 = new Thread(runnable, "t3");
    try {
        t1.start();
        t1.join();//主线程阻塞
        t2.start();
        t2.join();
        t3.start();
        t3.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

       join源码解析(代码不多,可以去看一下):
       注意当前锁住是整个对象。我们在A线程中调用B线程的Join方法,也就是B线程充当了这把锁,但调用者是A线程,也就是说挂起来的是A线程(谁调用wait谁休眠),当B线程还活着,就一直wait,只有当B线程执行完了,才会被唤醒,所以易推测出当B线程执行完毕会有一个收尾工作:使用notify方法,不然A线程就会一直挂着了,此代码可以在JVM源码中看到。


线程池

线程池种类:
  1. FixedThreadPool,特点:固定池子中线程的个数。使用静态方法newFixedThreadPool()创建线程池的时候指定线程池个数。
  2. CachedThreadPool(弹性缓存线程池),特点:用newCachedThreadPool()方法创建该线程池对象,创建之初里面一个线程都没有,当execute方法或submit方法向线程池提交任务时,会自动新建线程;如果线程池中有空余线程,则不会新建;这种线程池一般最多情况可以容纳几万个线程,里面的线程空余60s会被回收。
  3. SingleThreadPool(单线程线程池),特点:池中只有一个线程,如果扔5个任务进来,那么有4个任务将排队;作用是保证任务的顺序执行。
  4. newScheduleThreadPool
    创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

       下面解释下一下构造器中各个参数的含义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒

  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

       ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

  1. 在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。
  2. 在 ThreadPoolExecutor.CallerRunsPolicy中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  3. 在 ThreadPoolExecutor.DiscardPolicy中,不能执行的任务将被删除。
  4. ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
使用示例:
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy  ();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 200,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<>(2),
                handler);
队列:
  1. LinkedBlockingQueue的容量是没有上限的(说的不准确,在不指定时容量为Integer.MAX_VALUE,不要然的话在put时怎么会受阻呢),但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。

  2. ArrayBlockingQueue在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队 列,此队列按 FIFO(先进先出)原则对元素进行排序。

  3. PriorityBlockingQueue是一个带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(看了一下源码,PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。另外,往入该队列中的元 素要具有比较能力。

  4. DelayQueue(基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。


Java异常处理机制

异常处理
       非检查异常(unckecked exception)红框:Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。

       检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

try catch
	try{
	     //try块中放可能发生异常的代码。
	     //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
	     //如果发生异常,则尝试去匹配catch块。
	 
	}catch(SQLException SQLexception){
	    //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
	    //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
	    //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
	    //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
	    //如果try中没有发生异常,则所有的catch块将被忽略。
	 
	}catch(Exception exception){
	    //...
	}finally{
	 
	    //finally块通常是可选的。
	   //无论异常是否发生,异常是否匹配被处理,finally都会执行。
	   //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
	  //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 
	}
  1. try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
  2. 每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。
  3. java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
  4. 有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 ) 而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
throws 函数声明:(重要)

       throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。
       throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
       采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

	public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
	{ 
	     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
	}
手动抛异常:
	public void save(User user)
	{
	      if(user  == null) 
	          throw new IllegalArgumentException("User对象为空");
	      //......
	 
	}

Java jvm 异常处理:

       如果在执行的方法过程中抛出异常,JVM必须找到能捕获该异常的catch块

  1. 它首先观察当前方法是否存在catch块,如果存在,就执行该catch代码块
  2. 如果不存在,JVM会从从调用栈中弹出该方法的栈结构,继续到前一个方法中查找合适的catch块。
  3. 当JVM追溯到调用栈的最底部的方法,如果仍然没有找到处理该异常的代码块,将调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息随后终止整个应用程序。

异常链:

       异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

自定义异常:
public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException()
    {
        super();
    }
 
    public IOException(String message)
    {
        super(message);
    }
 
    public IOException(String message, Throwable cause)
    {
        super(message, cause);
    }
 
    public IOException(Throwable cause)
    {
        super(cause);
    }
}

Lock实现方式

ReentrantLock:

       ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;

  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
           获取锁:

     final boolean nonfairTryAcquire(int acquires) {
         final Thread current = Thread.currentThread();
         int c = getState();
         //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
         if (c == 0) {
             if (compareAndSetState(0, acquires)) {
                 setExclusiveOwnerThread(current);
                 return true;
             }
         }
         //2.若被占有,检查占有线程是否是当前线程
         else if (current == getExclusiveOwnerThread()) {
             // 3. 再次获取,计数加一
             int nextc = c + acquires;
             if (nextc < 0) // overflow
                 throw new Error("Maximum lock count exceeded");
             setState(nextc);
             return true;
         }
         return false;
     }
    

       释放锁:

	protected final boolean tryRelease(int releases) {
	    //1. 同步状态减1
	    int c = getState() - releases;
	    if (Thread.currentThread() != getExclusiveOwnerThread())
	        throw new IllegalMonitorStateException();
	    boolean free = false;
	    if (c == 0) {
	        //2. 只有当同步状态为0时,锁成功被释放,返回true
	        free = true;
	        setExclusiveOwnerThread(null);
	    }
	    // 3. 锁未被完全释放,返回false
	    setState(c);
	    return free;
	}

       ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略

公平锁与非公平锁:

       公平锁会对前驱结点进行判断,如果有前驱节点(也就是在之前队列中放入过线程)进行判断。故该策略导致严格按照时间进行。
       非公平锁不用判断。

       ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
       https://blog.youkuaiyun.com/yanyan19880509/article/details/52435135

volatile
  • volatile与可见性
           可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

           Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。

           前面的关于volatile的原理中介绍过了,Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

  • volatile与有序性
           有序性即程序执行的顺序按照代码的先后顺序执行。
           除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。
           而volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。
           普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。
           volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。

  • volatile与原子性
           原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
           线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

           synchronized关键字为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。
    所以,volatile是不能保证原子性的。

       在以下两个场景中可以使用volatile来代替synchronized:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
  2. 变量不需要与其他状态变量共同参与不变约束。
    除以上场景外,都需要使用其他方式来保证原子性,如synchronized或者concurrent包。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值