线程篇
什么是线程?
线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中.是进程中的实际运作单位。程序员可以通过它进行多处理器编程.你可以使用多线程对运算密集型任务提速。比如.如果一个线程完成一个任务要100 毫秒.那么用十个线程完成改任务只需10 毫秒。
线程和进程有什么区别?
线程是进程的子集.一个进程可以有很多线程.每条线程并行执行不同的任务。不同的进程使用不同的内存空间.而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。
如何实现多线程
四种方式:
- 继承Thread类:Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
- 实现Runnable接口:如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。
- 实现Callable接口:如果线程有返回值,那就必须实现Callable接口,如果没有返回值,则必须实现Runnable接口。
- 使用线程池的方式:线程和数据库连接这些资源都是非常宝贵的。如果每次使用时都去创建线程,那么会极大的占用系统资源,程序效率会降低。
常见线程池有哪些
java提供的线程池的顶级接口是Executor,但是它本身并不是一个线程池,而是一个执行线程的工具。真正的线程池接口时ExecutorService。常见线程池的方式有以下几种:
- newFiexdThreadPool:创建固定线程数量的线程池,采用共享的方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
- newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- newCachedThreadPool:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(newRunnable(){ @Override public void run(){ System.out.println("延迟三秒"); } }, 3, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ @Override public void run() { System.out.println("延迟1秒后每三秒执行一次"); } },1,3,TimeUnit.SECONDS);
- newSingleThreadExecutor:Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
线程什么周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
- 新建状态(new):当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
- 就绪状态(runnable):当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
- 运行状态(running):如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
- 阻塞状态(blocked):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
- 死亡状态(dead):线程正常结束、出现异常或者调用stop方法。
阻塞状态分为三种:
- 等待阻塞(o.wait->等待对列):运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞(sleep/join):运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程池原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
线程池的组成
一般的线程池主要分为以下4个组成部分:
- 线程池管理器:用于创建并管理线程池。
- 工作线程:线程池中的线程 。
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行。
- 任务队列:用于存放待处理的任务,提供一种缓冲机制。
线程复用
每一个 Thread 的类都有一个 start 方法。 当调用start启动线程时Java虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。
Java线程池工作过程
1.线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2.当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
线程优先级
- 通过线程类中提供的方法,完成对线程优先级的设置,优先级高的线程在前一段的时间内执行的次数要多一些,但不一定会先执行,也不一定先执行完。线程优先级低的线程,在后有一的时间中,执行的次数要多一些,不一定就后执行,也不一定就后执行完。
- setPriority(int newPriority)通过给定的数字完成对线程优先级的设置,数字越大,优先级就越高;数字的范围:1-10之间,最大为10,优先级最高,最小为1,优先级最低
- 有三个设置优先级的常量
MAX_PRIORITY 10 最大的优先级
MIN_PRIORITY 1 最小的优先级
NORM_PRIORITY 5 默认的优先级
线程常用方法
- 线程等待(wait):调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块中。
- 线程睡眠(sleep):sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep(long)会导致线程进入TIMED-WATING状态,而wait()方法会导致当前线程进入WATING状态。
- 线程让步(yield):yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
- 线程中断(interrupt) 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。1. 调用interrupt()方法并不会中断一个正在运行的线程。也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。 2. 若调用sleep()而使线程处于TIMED-WATING状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束TIMED-WATING状态。
3. 许多声明抛出InterruptedException的方法(如Thread.sleep(long mills方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterrupted()方法将会返回false。 4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。 - 等待其他线程终止(join):join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
- 线程唤醒(notify):Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
- isAlive(): 判断一个线程是否存活。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
- setName(): 为线程设置一个名称。
- setPriority(): 设置一个线程的优先级。
- getPriority()::获得一个线程的优先级。
synchronized和ReentrantLock的区别
1、共同点
- 都是用来协调多线程对共享对象、变量的访问。
- 都是可重入锁,同一线程可以多次获得同一个锁。
- 都保证了可见性和互斥性。
2、不同点
- ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁
- ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以实现公平锁
- ReentrantLock通过Condition可以绑定多个条件
- 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。
线程让出cpu的情况
- 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
- 当前运行线程结束,即运行完run()方法里面的任务。
Java中用到的线程调度
- 抢占式调度: 抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
- 协同式调度: 协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
JVM的线程调度实现(抢占式调度)
java使用的线程调使用抢占式调度,Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
乐观锁和悲观锁
- 乐观锁:乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间戳就是不加锁,通过时间戳来控制并发出现的问题。
- 悲观锁:悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允许其他用户访问那部分数据。