突然整理这篇博客是同事碰到个多线程的问题,问我可我没回答上来。java多线程是面试必问,也是一个讲不完的话题,下面是一些我整理的知识点。加油!
进程与线程
进程和线程都是系统的调度单位,但是相对于进程,线程则是系统调度的最小单位。对于一个程序来说,一个程序存在一个进程,但是可以存在多个线程,当进程结束,那么依赖他的线程也将全部结束。反之则不然。
线程的创建
在java中创建线程的方式主要有三种,先讲常用的两种:
1.继承Thread
1 | package top.huyuxin.thread; |
1 | Exthread exthread=new Exthread("exthread"); |
通过继承Thread类来创建线程,我们重写了他的其中一个构造,用来传递当前线程的名字进来,在执行的时候将其打印。(很多新手会直接使用线程的对象来调用run方法来开启线程这是错误的方式,Thread再神秘他也是一个类,通过类对象调用方法,那么他是不会新建一个线程的。)
2.实现Runnable接口
1 | package top.huyuxin.thread; |
当然即使实现了Runable,他也还只还是一个实现Runable的类而已,开启线程还是需要依赖Thread的另一个构造器来生成Thread对象来开启线程。1
2
3Imrunable imrunable=new Imrunable("imthread");
Thread imThread=new Thread(imrunable);
imThread.start();
3.实现Callable接口
当初别人跟我说第三种方法时候,我说还有这种操作?我在Thread的构造中没见过啊
然后他给我看了实现过程
1 | import java.util.concurrent.Callable; |
1 | Imcallable imcallable=new Imcallable("imcallable"); |
我又纳闷了Thread的构造也没传 FutureTask< V>这个的构造啊。
看了 FutureTask< V>这个类的源码才发现他实现了RunnableFuture< V>这个接口,而RunnableFuture< V>有继承了两个接口Runnable,Future< V>(extend用于类只能是单继承,用于接口可多继承)1
2
3
4
5
6
7
8
9
10
11public class FutureTask<V> implements RunnableFuture<V> {
//省略实现
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
所以实现Callable接口的方法用的是Thread(Runnable target)
这个构造器。
需要注意的是:Java中的多线程是一种抢占机制而不是分时机制。抢占机制指的是有多个线程处于可运行状态,但是只允许一个线程在运行,他们通过竞争的方式抢占CPU。
线程的状态(State):
java的源码将其分为六种状态,
1 | public enum State { |
但是我们为了更好理解,一般都将其分为以下五种状态,将WAITING和TIMED_WAITING合二为一:
新生状态(New):
当一个线程的实例被创建即使用new关键字和Thread类或其子类创建一个线程对象后,此时该线程处于新生(new)状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(not alive);
就绪状态(Runnable):
通过调用线程实例的start()方法来启动线程使线程进入就绪状态(runnable);处于就绪状态的线程已经具备了运行条件,但还没有被分配到CPU即不一定会被立即执行,此时处于线程就绪队列,等待系统为其分配CPCU,等待状态并不是执行状态; 此时线程是活着的(alive);
运行状态(Running):
一旦获取CPU(被JVM选中),线程就进入运行(running)状态,线程的run()方法才开始被执行;在运行状态的线程执行自己的run()方法中的操作,直到调用其他的方法而终止、或者等待某种资源而阻塞、或者完成任务而死亡;如果在给定的时间片内没有执行结束,就会被系统给换下来回到线程的等待状态;此时线程是活着的(alive);
阻塞状态(Blocked):
通过调用join()、sleep()、wait()或者资源被暂用使线程处于阻塞(blocked)状态;处于Blocking状态的线程仍然是活着的(alive)
死亡状态(Dead):
当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达死亡(dead)状态。此时可能仍然存在一个该Thread的实例对象,当该Thready已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的call stack已经被dissolved。一旦某一线程进入Dead状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于Dead状态的线程调用start()方法,会出现一个运行期(runtime exception)的异常;处于Dead状态的线程不是活着的(not alive)。
线程的方法(Method)、属性(Property)
1)优先级(priority)
每个类都有自己的优先级,一般property用1-10的整数表示,默认优先级是5,优先级最高是10;优先级高的线程并不一定比优先级低的线程执行的机会高,只是执行的机率高;默认一个线程的优先级和创建他的线程优先级相同;
1 | /** |
2)Thread.sleep()/sleep(long millis)
当前线程睡眠/millis的时间(millis指定睡眠时间是其最小的不执行时间,由于java的多线程是抢占机制,sleep(millis)休眠到达后,无法保证会被JVM立即调度);sleep()是一个静态方法(static method) ,睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,所以他不会使得其他的线程也处于休眠状态;线程sleep()时不会失去拥有的对象锁。
作用:保持对象锁,让出CPU,调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留一定的时间给其他线程执行的机会;
3)Thread.yield()
让出CPU的使用权,给其他线程执行机会、让同等优先权的线程运行(但并不保证当前线程不会被JVM再次调度、使该线程重新进入Running状态),如果没有同等优先权的线程,那么yield()方法将不会起作用。
4)thread.join()
在一个线程里,另一个线程调用join方法,那么所在的这个线程将进入阻塞态,直到另一个线程执行完毕。
5)object.wait()
当一个线程执行到wait()方法时,他就进入到一个和该对象相关的等待池(Waiting Pool)中,同时失去对象锁—暂时的,wait后还要返还对象锁。当前线程必须拥有当前对象的锁,如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常,所以wait()必须在synchronized block中调用。并且notify()和notifyAll()必须在具有调用wait()相同的对象锁的synchronized block中调用。
6)object.notify()/notifyAll()
唤醒在当前对象等待池中等待的第一个线程/所有线程。notify()/notifyAll()也必须拥有相同对象锁,否则也会抛出IllegalMonitorStateException异常。当多个线程被阻塞notify()的第一个线程将是一个随机线程,notifyAll()则会将全部阻塞线程唤醒,至于哪个线程进入运行态则不得而知。
7)thread.setDaemon(boolean on)
在创建线程时如果当前线程是守护线程,那么创建的子线程默认也是守护进程,当进程结束,那么他的守护进程不管是否已经结束都将被销毁。
设置某个线程为守护线程时,应该在其调用start()之前。否则将抛出异常 IllegalThreadStateException,。守护线的好处就是你不需要关心它的结束问题。当主线程结束他也将结束。
8)Synchronized
Synchronized 可以修饰代码块,方法,静态方法,类.
- 当修饰代码块时当前Thread持有的对象锁是传入的形参obj
synchronized (Obejct obj)
- 当修饰方法时,当前Thread持有的对象锁是调用方法的对象等同于
synchronized(this)
- 当修饰静态方法时,等同于
synchronized(Object.class)
- 当修饰类时,这个类所有的方法都等同于被 synchronized修饰
每个Synchronized Block/方法只有持有调用该方法被锁定对象的锁才可以访问,否则所属线程阻塞;机锁具有独占性、一旦被一个Thread持有,其他的Thread就不能再拥有(不能访问其他同步方法),方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
对象锁
在上面提到了很多次的对象锁,那么对象锁到底是什么呢?这就要说到java的monitor了
synchronized, wait, notify 是任何对象都具有的同步工具。
他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器/对象锁,来监测并发代码的重入。在非多线程编码时该对象锁不发挥作用,反之如果在synchronized 范围内,对象锁发挥作用。
wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个对象锁(某对象的对象锁)。这意味着wait(暂时失去对象锁)之后,其他线程可以进入同步块执行。
当某代码并不持有对象锁的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的对象锁不同,同样会抛出此异常。
那么如果在拥有对象锁的代码块里再次执行需要当前对象锁时能否进入呢?答案是肯定的,每一次重入相同对象锁的代码monitor的计数器将+1,退出时计数器将-1,当计数器为0时则完全释放了这个对象锁。
正确结束线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!(突然的结束这将使得对象锁的状态不可控)想要安全有效的结束一个线程,可以使用下面的方法:
• 正常执行完run方法,然后自然的结束掉;
• 控制循环条件和判断条件的标识符来结束掉线程。
1 | class MyThread extends Thread { |
通过标志位来结束线程使用起来十分简单的方法。
线程同步
在多线程并发的时候对于同一资源操作,数据变得十分不可靠。加入对象锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
1、synchronized
即有synchronized关键字修饰的方法。由于java的每个对象都有一个对象锁,当用此关键字修饰方法时,对象锁会保护整个方法。在调用该方法前,需要获得对象锁,否则就处于阻塞状态。
1 | public synchronized void save(){} |
这与所持有的锁是一致的。1
2
3
4public void save(){
synchronized(this){
}
}
当synchronized修饰静态方法时,1
public static synchronized void save(){}
他持有的锁等同于当前类的class对象,他将会锁住整个类
1 | public void save(){ |
当synchronized修饰类时,那么等同于这个类所有的方法都被synchronized修饰。
同步是一个开销很高的操作因为对象锁是一个互斥锁,当一个线程得到了,别的线程想要访问将处于阻塞状态。直到已获取对象锁的线程释放才有机会获得。相对于同步方法,同步代码块是一个相对能够接受的方式。
2.volatile
线程栈
线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈,是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。
3.原子类
java提供的原子操作可以原子更新的
基本类型有以下三个:
AtomicBoolean
AtomicInteger
AtomicLong
原子更新数组,Atomic包提供了以下几个类:
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
原子更新引用类型,也就是更新实体类的值,比如:
AtomicReference:原子更新引用类型的值
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型
原子更新字段值:
AtomicIntegerFieldUpdater:原子更新整形的字段的更新器
AtomicLongFieldUpdater:原子更新长整形的字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型的更新器
那这些原子类是怎样实现原子操作的呢?可以拿AtomicInteger的getAndIncrement()方法实现来说明,看方法名就知道函数的功能了:1
2
3
4
5
6
7
8 public final int getAndIncrement() {
for (;;) {
int current = get(); // 取得AtomicInteger里存储的数值
int next = current + 1; // 加1
if (compareAndSet(current, next)) // 调用compareAndSet执行原子更新操作
return current;
}
}
他在里面写了个死循环然后不断的获取自己的值,直到获取的当前值相等执行set操作并return跳出死循环。
4、使用重入锁(Lock)实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
这个重入锁是synchronized的加强版!使用也十分的方便。并且加入了锁投票、定时锁等候和可中断锁等候的一些特性等待你去发掘。
1 | class Bank { |
线程间通信
1、借助于Object类的wait()、notify()和notifyAll()实现通信
线程执行wait()后,就放弃了运行资格,处于冻结状态;线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
notifyall(), 唤醒线程池中所有线程。
注: (1) wait(),notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。
1 | package top.huyuxin.thread; |
1 | package top.huyuxin.thread; |
1 | package top.huyuxin.thread; |
在使用过程中他们相互唤醒,轮流运行。1
2
3
4
5Resource resource =new Resource();
SyncSubThread syncSubThread =new SyncSubThread(resource);
SyncAddThread syncAddThread =new SyncAddThread(resource);
syncSubThread.start();
syncAddThread.start();
2、使用Condition控制线程通信
jdk1.5中,提供了多线程的升级解决方案为:
- (1)将同步synchronized替换为显式的Lock操作;
- (2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
- (3)一个Lock对象上可以绑定多个Condition对象,唤醒指定的线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
将上面Resouce类修改一番,其中Condition类的等待方法是await(),唤醒方法是signal(),signalAll(),其中Condition的对象是从当前lock中获取而不是通过new。
1 | package top.huyuxin.thread; |
我们启动三个线程,他们会相互唤醒,add唤醒mul,mul唤醒sub,sub唤醒add,依次循环。1
2
3
4
5
6
7Resource resource =new Resource();
SyncSubThread syncSubThread =new SyncSubThread(resource);
SyncAddThread syncAddThread =new SyncAddThread(resource);
SyncMulThread syncMulThread =new SyncMulThread(resource);
syncSubThread.start();
syncAddThread.start();
syncMulThread.start();
使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
3.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接口包含如下5个实现类:
- ArrayBlockingQueue :基于数组实现的BlockingQueue队列。
- LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
- PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
- SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
- DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法), DelayQueue根据集合元素的getDalay()方法的返回值进行排序。
看下ArrayBlockingQueue的构造,原来也是使用newCondition实现
1 | public ArrayBlockingQueue(int capacity, boolean fair) { |
使用样例:
1 | import java.util.concurrent.ArrayBlockingQueue; |
内容有点多,下篇整理线程池以及线程相关的类ThreadLocal,ThreadGroup等