今天刚刚参加了一次面试,被面试官的关于线程的提问生生碾压了。重整旗鼓,总结一下关于线程的基础知识,相信也可以帮到一批小伙伴的。首先分享一下基础知识。南玻万:0
线程与进程的区别
区别详解请移步链接中的文章已经把区别整理的很完善了,同学们可以自行移步查看。我在这里写一下我自己总结的它们之间的区别:
- 它们是不同的操作系统管理资源的方式。
- 进程是系统进行资源分配和调度的一个独立单位;
- 线程是CPU进行资源分配和调度的基本单位;
- 一个程序至少有一个进程,一个进程又至少有一个线程。
- 进程有自己独立的地址空间,一个进程崩溃后,一般不会对其他进程产生影响。而线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但线程没有单独的地址空间。一个线程死掉就等于整个进程死掉,所以多进程的程序比多线程的程序更健壮。但进程切换时,耗费资源较大,效率要差一些。
线程的状态
下面这张图片展示了线程的所有状态和状态之间的互相转换:
- 可运行(runnable):线程创建后调用了.start()方法。该线程位于可运行的线程池中,等待被线程调度选中,获得CPU的资源;
- 运行(running):可运行状态(runnable)的线程获得了CPU的时间片(timeslice),执行线程内的代码;
- 阻塞(block):阻塞状态是指线程暂时放弃了CPU的使用权,即让出了CPU的时间片。阻塞状态的线程直到转换成可运行状态进入到可运行线程池中,才会有机会再次获得CPU的时间片进入到运行状态。使线程进入阻塞状态的情况由一下几种:
- 等待阻塞:线程执行了wait()方法,JVM会将线程放入到等待队列(waitting queue)中。
- 同步阻塞:运行的线程在获取对象的同步锁时,如该同步锁被别的线程占用,则JVM会把该线程放入到锁池(lock pool)中。
- 其他阻塞:运行中的线程执行了Thread.sleep()方法或b.join()方法,或者当前线程执行到了I/O操作时。JVM会把线程切换到阻塞状态,当sleep()方法休眠结束时、b线程执行完成后,I/O操作完成后,该线程又会转换到Runnable状态。
- 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
同步锁(synchonrize)的介绍
相信很多同学应该都和我差不多只知道这个关键字的简单实用,对于很多细节其实并不是很清楚,这里我同大家一起分享关于同步锁的正确使用。
首先我们大家都应该知道每个对象都有一个锁标志,被synchonrize
关键字修饰的变量或方法将被上锁,同一时刻只能被单一线程访问。当前线程访问完数据后释放锁标志,其他线程才可以进行访问。这里还需要给大家补充一下线程常用方法与同步锁之间存在的关系,以下表格可以清晰的展示这一点:
方法 | 是否释放锁 | 备注 |
---|---|---|
wait | 是 | wait和notify/notifyAll是成对出现的, 必须在synchronize块中被调用 |
sleep | 否 | 可使低优先级的线程获得执行机会 |
yield | 否 | yield方法使当前线程让出CPU占有权, 但让出的时间是不可设定的 |
对以上方法的备注我还引入一些更具体的解释(点击给进入原作者的文章)。如下:
sleep()
使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。
例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。
总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。yield()
该方法与sleep()类似,只是不能由用户指定暂停多长时间。这里还需补充一点,yield()方法对应了如下操作: 先检测当前是否有相同优先级的线程处于同可运行状态, 如有, 则把 CPU 的占有权交给此线程, 否则继续运行原来的线程.。所以yield()方法称为“退让”, 它把运行机会让给了同等优先级的其他线程。wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取,wait()有出让Object锁的语义, 要想出让锁, 前提是要先获得锁, 所以要先用synchronized获得锁之后才能调用wait(), notify原因类似。
wait()方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。
notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
同步锁(synchonrize)的使用
synchronize关键字主要有下面5种用法
- 在方法上进行同步, 分为(1)instance method/(2)static method, 这两个的区别后面说
- 在内部块上进行同步, 分为(3)synchronize(this), (4)synchonrize(XXX.class), (5)synchonrize(mutex)
代码示例如下:
private int value = 0;
private final Object mutex = new Object();
public synchronized int incAndGet0() {
return ++value;
}
public static synchonrize int incAndGet1();
public int incAndGet2() {
synchronized(this){
return ++value;
}
}
public int incAndGet3() {
synchronized(SyncMethod.class){
return ++value;
}
}
public int incAndGet4() {
synchronized(mutex){
return ++value;
}
}
- 作为修饰符加在方法声明上, synchronized修饰非静态方法时表示锁住了调用该方法的堆对象, 修饰静态方法时表示锁住了这个类在方法区中的类对象。
- 关于Java中的堆、栈和方法区,请大家移步很详细的介绍了以上内容,感觉作为程序员真是学海无涯啊。
- synchronized(X.class) 使用类对象作为监控。 同一时间只有一个线程可以能访问块中资源。
- synchronized(this)和synchronized(mutex) 都是对象锁, 同一时间每个实例都保证只能有一个实例能访问块中资源。
以上就是关于synchonrize关键字的基础知识。其他还有一些关于synchronized和volatile比较和synchonrize和juc中的锁比较
这里也简单介绍一下:
- volatile
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。- 互斥,即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
- 可见性,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量具有 synchronized的可见性特性,Volatile是轻量级的synchronized。
当且仅当下面条件全部满足时, 才能使用volatile
- 对变量的写入操作不依赖于变量的当前值, (++i/i++这种肯定不行), 或者能确保只有单个线程在更新
- 该变量不会与其他状态变量一起纳入不变性条件中
- 访问变量时不需要加锁
- ReentrantLock
ReentrantLock在内存上的语义于synchronize相同, 但是它提供了额外的功能, 可以作为一种高级工具. 当需要一些 可定时, 可轮询, 可中断的锁获取操作, 或者希望使用公平锁, 或者使用非块结构的编码时 才应该考虑ReetrantLock。
以上就是这两种锁的比较的简单介绍,想要深入学习的同学请大家自行搜索学习吧。最后送给正在进行面试同学们的一句话:面试官虐我千百遍,我待面试官如初恋。---必须正能量,清喷。