前言
一般而言,一个线程往往不是孤立存在的,常常需要和其它线程通信,以执行特定的任务。线程与线程间必定有一个信息传递的渠道。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的。线程间的通信涉及到4个问题:
- 线程间如何传递信息
- 线程间如何同步
- 线程间如何调度
- 如何避免死锁
在Java多线程编程环境下,我们一般采用如下两种方式:
- 使用synchronized和volatile关键字
- 使用等待/通知机制
使用synchronized和volatile关键字
synchronized关键字
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
synchronized同步方法
public synchronized void count(){
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName()+" "+ i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意点:
当A线程调用anyObject中synchronized修饰的X方法时,A线程就获得了对象的锁,B线程调用anyObject中synchronized修饰的方法(包括上述X方法)时,必须等到A线程将X方法执行完毕,但是B线程可以调用其他任意非synchronized修饰的方法。
关键字synchronized拥有锁重入功能,即当一个线程获得一个对象锁后,可以再次请求并获得该对象锁。也就是说synchronized方法内部调用本对象的其他synchronized方法时并不会被阻塞。
当线程执行的代码出现异常时,其所持有的锁会自动释放。此外synchronized不具有继承性。
synchronized同步块
public void count(){
for(int i = 0; i < this._num; i++){
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName()+" "+ i);
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" "+ i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void count(Integer num){
for(int i = 0; i < num; i++){
synchronized (num) {
try {
System.out.println(Thread.currentThread().getName()+" "+ i);
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+" "+ i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized static void print(){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
注意点:
当线程执行synchronized(this)同步代码块时,其获得的是当前对象的锁,其他线程调用对象方法时互斥条件与synchronized方法场景相同;
当线程执行synchronized(非this对象x)同步代码块时,如果监视对象x不同,则线程会异步执行;由于String对象存在常量池问题,一般不选用String类型对象作为监视对象;
当线程执行synchronized修饰的static方法时,其获得的是类对象锁,并非实例对象锁。由于监视对象不一样,synchronized修饰的static方法和非static方法可以异步访问。
volatile关键字
关键字volatile告知程序任何对该变量的而访问均需要从共享内存中获取,而对它的修改必须同步刷新回共享内存,如此使得变量在各线程之间可见。
注意点:
volatile只能修饰变量;
volatile能保证数据的可见性,但是无法保证原子性。
使用等待/通知机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法后进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续的操作。
public class ThreadA implements Runnable {
private Object object;
public ThreadA(Object object){
this.object = object;
}
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+" Start");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" End");
}
}
}
public class ThreadB implements Runnable {
private Object object;
public ThreadB(Object object){
this.object = object;
}
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+" Start");
object.notify();
System.out.println(Thread.currentThread().getName()+" End");
}
}
}
注意点:
Java为每个Object都实现了wait()和notify()方法,但它们必须用在被synchronized同步的Object的临界区内;
线程执行完wait()方法后立即释放锁对象,但是线程执行完notify()方法后并不立即释放锁对象,而是等到执行完synchronized同步块后释放。
notify()方法一次只随机唤醒一个线程;notifyAll()方法可以使得所有正在等待队列中等待同一共享资源的线程从等待状态退出,进入可运行状态。此时优先级最高的那个线程最先执行,也有可能随机执行,这取决于虚拟机的实现。
死锁
死锁,是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁的必要条件:
互斥,线程之间互斥访问同一资源;
请求并保持,线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它线程占有,此时请求线程阻塞,但又对自己已获得的其它资源保持不放;
不可剥夺,线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
循环等待,在发生死锁时,必然存在一个线程——资源的环形链。
一般来说,我们通过破坏上诉四个条件中的一个或多个便能够达到预防死锁的目的。
总结
线程在对共享对象访问时需要同步机制,而对私有变量访问时并不存在此问题,这是我们研究线程通信的一个前提。除了上述的方法外,线程还可以使用join()方法和ThredLocal完成线程间通信,后者后续会有详细讲解。