进程、线程、线程池、线程安全
进程
- 一个计算机程序的运行实例,包含了需要执行的指令;
- 有自己的独立地址空间,包含程序内容和数据;
- 不同进程的地址空间是互相隔离的;
- 进程拥有各种资源和状态信息,包含打开的文件、子进程和信号处理;
进程主要包含的表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;
线程
表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进程锁拥有的内存和其他资源;
线程是java.lang.Thread;
线程状态
- 创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
- 就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
- 阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
- 死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。
线程主要有两种方式实现:
- 继承Thread、实现Runnable;其主要逻辑都是实现run()函数,
- Thread可以直接通过start()启动线程;
- Runnable就必须新建一个Thread对象进行执行start()进行启动线程;
wait()、notify()、notifyAll()方法进行使线程休眠或者唤醒;
对象的
wait()
:将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()
方法,线程A进入B对象的等待池,并且释放对B的锁。(这里线程A必须持有B的锁,所以调用的代码必须在synchronized
修饰下,否则直接抛出java.lang.lIIegalMonitorStateException
异常)线程重新执行,需要外部唤醒;
sleep()是线程休眠一段时间后线程自动继续执行逻辑;休眠过程中不释放对象锁,线程监控状态一直存在;
- notify()唤醒对应的线程;
- notifyAll()使唤醒全部线程;
- notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法;
- notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
- notify则文明得多他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们
java 两种锁机制synchronized和Lock
synchronized 是在 JVM 层面上实现的,synchronized在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。
- synchronized作用在方法上时,锁住的便是对象实例(this);
- synchronize作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;
- synchronized作用于某一个对象实例时,锁住的便是对应的代码块;
- synchronized 用在方法签名上,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。
当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行方法test,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。否则synchronized关键字将失去意义。
Lock的锁定是通过代码实现的,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。
一般使用ReentrantLock(重入锁)类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和 signalAll
- 某个线程在等待一个锁的控制权的这段时间需要中断
- 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
- 具有公平锁功能,每个到来的线程都将排队等候
线程安全:在多线程环境下,不管什么时候执行同一段代码得到结果都是确定的,既是线程安全;
线程池主要用过是ThreadPoolExcutor函数,通过定义线程池的核心池大小、最大线程数,通过实现Runnable接口的线程交由线程池执行;
常用函数说明
- sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- join():指等待t线程终止。
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。- yield():暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。- setPriority(): 更改线程的优先级。
- interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭
sleep()和yield()的区别
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。
一个典型的倚赖数据库的悲观锁调用:
select * from account where name=”Erica” for update
这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。
本次事务提交之前(事务提交时会释放事务过程中的锁,外界无法修改这些记录。
Hibernate的悲观锁,也是基于数据库的锁机制实现。乐观锁(Optimistic Locking)
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额,如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间,数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于
数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version字段,当前值为1;而当前帐户余额字段(balance)为$100。
- 操作员A 此时将其读出(version=1),并从其帐户余额中扣除 50( 100-$50)。
- 在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其帐户余额中扣除 20( 100-$20)。
- 操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。
- 操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员B提交的
数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记录当前版本才能执行更新“的乐观锁策略,因此,操作员B 的提交被驳回。
这样,就避免了操作员B 用基于version=1 的旧数据修改的结果覆盖操作员A的操作结果的可能。
从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员A和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。
需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的
生产力。